关闭

12.1 剪贴板的简单用法

标签: windows编程剪贴板
997人阅读 评论(0) 收藏 举报
分类:

摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P447

        让我们先看看把数据传入剪贴板(剪切和复制)和从剪贴板中取得数据(粘贴)的代码。

12.1.1  剪贴板数据的标准格式

        Windows 支持各种预定义的剪贴板格式,这些格式在 WINUSER.H 中定义并有前缀为 CF 的标识符

        首先,在剪贴板中可以存储三种文本数据类型,相应的有一种相关的剪贴板格式。

  • CF_TEXT    一种以 NULL 结尾的 ANSI 字符集字符串,字符串的每行结尾处含有一个回车换行符。这是最简单的剪贴板数据格式。要被传输到剪贴板的数据存储在内存块中,传输时使用指向此内存块的句柄。(我将很快介绍这个概念。)该内存块会成为剪贴板的属性,因此创建这个内存块的程序不应该继续使用它
  • CF_OEMTEXT    包含文本数据(与 CF_TEXT 类似)但使用 OEM 字符集的内存块。Windows 程序通常不用担心这点;在窗口中运行 MS-DOS 程序时如果使用剪贴板则需要用到这种类型。
  • CF_UNICODETEXT    包含 Unicode 文本的内存块。类似于 CF_TEXT,每行以回车换行符结束,字符 NULL(两个零字节)标志着整个数据的结束。CF_UNICODETEXT 只在 Windows NT 下支持。
  • CF_LOCALE    指向区域设置标识符的句柄,该标识符标明了与剪贴板相关的区域设置。

        此外还有两种剪贴板格式,它们在概念上和 CF_TEXT 格式类似(就是说,它们是基于文本的),但是它们不一定以 NULL 结尾,因为格式本身定义了数据的结尾。这些格式现在很少用到

  • CF_SYLK    含有微软符号链接(Symbolic Link)格式数据的内存块。这种格式被用来在微软的 Multiplan、Chart 和 Excel 程序之间交换数据。它是一种 ASCII 格式,每行以回车换行符结束。
  • CF_DIF    含有数据交换格式(DIF)数据的内存块。这是一种由 Software Arts 设计,用来把数据传输到 VisiCalc 电子表格程序的格式。它也是一种 ASCII 格式,每行以回城换行符结束。

        与位图相关的剪贴板格式有三种。位图即对应于输出设备像素的矩形位数组。位图和这些位图剪贴板格式将在第 14 章和第 15 章详细讨论。

  • CF_BITMAP    设备相关位图。此位图由位图句柄传输到剪贴板中。再强调一遍,程序把位图传给剪贴板之后不应继续使用此位图
  • CF_DIB    定义了设备无关位图的内存。设备无关位图在第 15 章会给出描述。该内存块以位图信息结构开头,接着有可能是颜色表和位图的位。
  • CF_PALETTE    指向调色板的句柄。它通常和 CF_DIB 一起使用,用来定义在设备相关位图中使用的调色板。

        剪贴板里的位图数据也可以采用行业标准的 TIFF 格式存储。

  • CF_TIFF    包含标签图像文件格式(Tag Image File Format, TIFF)数据的内存块。这是由微软、Aldus 和惠普公司联合一些硬件制造商共同设计的格式。这种格式从惠普网站上可以找到。

        还有两种图元文件格式,我会在第 18 章详细描述。图元文件是以二进制形式存储的绘图命令的集合

  • CF_METAFILEPICT    基于 Windows 过去支持的图元文件的“图元文件图片”。
  • CF_ENHMETAFILE    指向 32 位 Windows 版本支持的增强型图元文件的句柄。

        最后还有一些其他不同的剪贴板格式:

  • CF_PENDATA    和 Windows 画笔扩展一起使用。
  • CF_WAVE    声音(波形)文件。
  • CF_RIFF    资源交换文件格式(RIFF)的多媒体数据。
  • CF_HDROP    和拖放服务一起使用的文件列表。

12.1.2  内存分配

        当程序把一些数据传到剪贴板时,程序必须分配一个内存块并移交给剪贴板。在本书先前的程序中,当我们需要分配内存时,只是调用了标准 C 运行时库支持的 malloc 函数。然而,由于剪贴板中存储的内存块必须在 Windows 下运行的应用程序之间共享,所以 malloc 函数不足以完成此任务。

        取而代之的是,我们必须研究在 Windows 早期设计的内存分配函数,那时的操作系统运行在 16 位实模式存储体系结构上。虽然这些函数通常没有什么用,但是现在仍然是受支持的,所以仍然可以用。

        可以用 Windows API 分配一块内存:

hGlobal = GlobalAlloc(uiFlags, dwSize);
这个函数有两个参数:一系列可能的标志和被分配内存块的字节大小。函数返回 HGLOBAL 类型的句柄,这个句柄被称为“指向全局内存块的句柄”(handle to a global memory block)或“全局句柄”。这个函数返回值为 NULL 时就表示没有足够的内存空间可以分配。

        尽管 GlobalAllo 的两个参数定义不同,但它们都是 32 位无符号整数。如果把第一个参数设为 0,那么使用的就是标志 GMEM_FIXED。这种情况下,GlobalAlloc 返回的全局句柄实际是一个指向被分配内存块的指针。

        如果想让内存块中的每个字节初始化为 0,则可以用标志 GMEM_ZEROINIT。在 Windows 头文件中,简短的 GPTR 标志把 GMEM_FIXED 和 GMEM_ZEROINIT 标志定义在一起:

#define GPTR (GMEM_FIXED | GMEM_ZEROINIT)

        还有一个内存重分配函数:

hGlobal = GlobalReAlloc(hGlobal, dwSize, uiFlags);
如果内存块扩大了,可以用 GMEM_ZEROINIT  标志把新分配的字节初始化为 0。

        取得内存块大小的函数如下:

dwSize = GlobalSize (hGlobal);
释放内存的函数是:

GlobalFree (hGlobal);

        早期的 16 位 Windows 版本会尽量避免 GMEM_FIXED 标志,因为 Windows 不能在物理内存中移动内存块。在 32 位 Windows 中,GMEM_FIXED 可以正常使用,因为它返回一个虚拟地址,操作系统可以通过改变页表移动物理内存里的内存块。在 16 位 Windows 上编程,推荐在 GlobalAlloc 中用 GMEM_MOVEABLE 标志。在 Windows 头文件中还定义了一个缩写标识符,用来把可移动内存初始化为 0:

#define GHND (GMEM_MOVEABLE |  GMEM_ZEROINIT)
GMEM_MOVEALBE 标志使 Windows 能在虚拟内存中移动内存块。这并不意味着内存块会在物理内存中移动,仅仅是此应用程序用来读/写内存块的地址可能会改变

        尽管 GMEM_MOVEABLE 经常用于 16 位 Windows 版本,但现在如今通常都不再使用了。虽然如此,如果应用程序频繁地分配、重分配、释放不同大小的内存块,应用程序虚拟地址空间就会变得七零八碎。你可能会把虚拟内存地址用完。如果这是个潜在的问题,就需要用可移动内存,具体用法如下。

        先定义一个指针(例如,指向整数类型的指针)和一个 GLOBALHANDLE 类型的变量:

int *p;
GLOBALHANDLE hGlobal;
然后分配内存,例如:

hGlobal = GlobalAlloc(GHND, 1024);

        和其他 Windows 句柄一样,不要太在意具体数字代表什么。只要存储下来就可以了。如果需要访问这个内存块,可调用以下函数把句柄转换为指针:

p = (int *) GlobalLock (hGlobal);
在内存块被锁定期间,Windows 会修复虚拟内存地址。内存块不会移动。访问完内存块之后,调用以下函数使 Windows 自由移动虚拟内存中的内存块:

GlobalUnLock (hGlobal);
为确保这个过程正确(并且体验早期 Windows 程序员所经历过的苦痛),应该在单个消息过程中锁定和解锁内存块。

        如果想释放内存,则调用 GlobalFree,传入句柄而不是指针。如果现在没有可访问的句柄,那么用以下函数即可:

hGlobal = GlobalHandle(p);

        可以在释放一个内存块之前多次锁定它。Windows 有一个锁计数器,内存块被自由移动前,每次锁定都要有一个对应的解锁。Windows 在虚拟内存中移动内存块不需要把字节从一个位置复制到另一个位置——它只需要操作页表。一般来说,在 32 位版本的 Windows 上,为应用程序分配可移动内存的唯一原因就是防止虚拟内存变得零碎用剪贴板时,也应该用可移动内存

        为剪贴板分配内存时,应该用 GlobalAlloc 函数,并同时使用 GMEM_MOVEABLE 和 GMEM_SHARE 标志作为参数。GMEM_SHARE 标志使内存块能被其他 Windows 应用程序共享。

12.1.3  把文本传到剪贴板

        假设你想把一个 ANSI 字符串传到剪贴板。你有一个指向这个字符串的指针(称作 pString),并且想传 iLength 个字符,它们也可能不是以 NULL 终止。

        首先得用 GlobalAlloc 来分配足够容纳这个字符串的内存块,并给终止符 NULL 留下空间:

hGlobal = GlobalAlloc (GHND | GMEM_SHARE, iLength + 1);
如果内存分配失败,hGlobal 的值为 NULL。如果分配成功,则锁定这个内存块并得到指向它的指针:

pGlobal = GlobalLock (hGlobal);
把字符串复制到全局内存块:

for (i = 0; i < iLength; ++ i)
    *pGlobal++ = *pString++;
不需要加终止符 NULL,因为 GlobalAlloc 的 GHND 标志在分配内存时把整个内存块置 0。解锁内存:

GlobalUnLock (hGlobal);

        现在你有了一个全局内存句柄,它指向一块包含以 NULL 为终止符的文本的内存块。要把这个内存块传入剪贴板,应打开剪贴板并且清空它:

OpenClipboard (hwnd);
EmptyClipboard();
通过使用 CF_TEXT 标识符,把内存句柄传给剪贴板,并且关闭剪贴板:

SetClipboardData (CF_TEXT, hGlobal);
CloseClipboard();http://write.blog.csdn.net/postedit/49906463
这就万事大吉了。

        以下是关于这个过程的一些规则。

  • 在处理单个消息的过程中调用 OpenClipboard 和 CloseClipboard。避免不必要地长期打开剪贴板。
  • 不要把一个锁定的内存句柄传给剪贴板。
  • 调用 SetClipboardData 后,不要继续使用该内存块。它已不再属于程序,你应该把该句柄当作是无效的。如果需要继续使用数据,应再复制一份或者从剪贴板中读取(下一小节会讲到)。在调用 SetClipboardData 和 CloseClipboard 之间,也可以继续引用内存块,但是不要使用传给 SetClipboardData 函数的全局句柄。你可以使用由这个函数返回的全局句柄。锁定这个句柄来访问内存。在调用 CloseClipboard 之前解锁这个句柄。

12.1.4  从剪贴板中取得文本

        从剪贴板中取得文本只比把文本传给剪贴板稍稍麻烦一点点。首先要确定剪贴板实际上 是否包含 CF_TEXT 格式的文本。最简单的方法之一是调用以下函数:

bAvailable = IsClipboardFormatAvailable (CF_TEXT);
如果剪贴板包含 CF_TEXT 数据,这个函数返回 TRUE(非 0)。在第 10 章的 POPPAD2 程序中,我们用这个函数检查 Edit 菜单上的 Paste 项是否可用。IsClipboardFormatAvailable 是剪贴板少数几个不需要打开剪贴板就能使用的函数之一。但是,在之后打开剪贴板以取得文本时,应该再次进行检查(用同样的函数或者其他某种方法),以确定 CF_TEXT 数据是否仍保存在剪贴板中。

        要把文本传出,先如下打开剪贴板:

OpenClipboard(hwnd);
取得指向文本的全局内存块的句柄;       
hGlobal = GetClipboardData (CF_TEXT);
如果剪贴板没有 CF_TEXT 格式的数据,此句柄为 NULL。这是判断剪贴板是否有文本数据的另一个方法。如果 GetClipboardData 返回 NULL,则应关闭剪贴板,其他什么都不要做。

        从 GetClipboardData 得到的句柄不属于你的程序,它属于剪贴板。这个句柄只在 GetClipboard 和 CloseClipboard 调用之间有效。你不能释放此句柄或是改变它应用的数据。如果想继续访问数据,应该复制内存块。

        以下是把数据复制到你程序里的方法。分配一个指向和剪贴板数据块一样大小的内存块的指针:

pText = (char *) malloc (GlobalSize (hGlobal));
记住,hGlobal 是调用 GetClipboardData 取得的全局句柄。现在锁定该句柄,得到一个指向剪贴板数据块的指针:

pGlobal = GlobalLock (hGlobal);
现在复制数据:

strcpy (pText, pGlobal);
或者也可以用简单的 C 代码:

while (*pText++ = *pGlobal++);

        解锁内存块之后关闭剪贴板:

GlobalUnlock (hGlobal);
CloseClipboard();
现在你有了一个 pText 指针,它指向你的程序所拥有的文本副本。

12.1.5  打开和关闭剪贴板

        无论何时都只有一个程序可以打开剪贴板。调用 OpenClipboard 的目的是当某个程序正在使用剪贴板时,防止剪贴板内容改变。OpenClipboard 会返回一个表示剪贴板是否已被成功打开的 BOOL 值。如果有另一个应用程序尚未关闭剪贴板,便无法打开它。如果每个程序都根据用户命令尽快打开和关闭剪贴板,你可能就不会遇到无法打开剪贴板这个问题。

        如果程序之间不那么谦让或者碰到抢占式多任务,可能就会有些麻烦。即使你的程序在把数据放到剪贴板和用户调用粘贴项之间这段时间里没有丢失输入焦点,也不要假设你传入的数据还在剪贴板里。后台进程在这段时间里有可能已经访问过剪贴板。

        还要当心涉及消息框时的一个更微妙的问题:在不能分配足够的内存把数据复制到剪贴板时,你或者想显示一个消息框。但是,如果这个消息框不是系统模态,那么用户可以在消息框显示时切换到另一个应用程序。所以,要么把消息框变成系统模态,要么在显示消息框前关闭剪贴板。

        如果在打开剪贴板的同时又显示了对话框,也可能会碰到问题。对话框的编辑字段会使用剪贴板来剪切和粘贴文本。

12.1.6  剪贴板和 Unicode

        到目前为止,我们仅仅讨论了用剪贴板传输 ANSI 文本(每个字符一个字节)。这是用 CF_TEXT 标识符时使用的格式。你可能还想了解 CF_OEMTEXT 和 CF_UNICODETEXT。

        我有个好消息:你只需在调用 SetClipboardData 和 GetClipboardData 时加上自己想要的文本格式,Windows 便会再剪贴板里处理所有的文本转换。例如,在 Windows NT 下,如果某程序用 CF_TEXT 剪贴板数据类型来使用 SetClipboardData,那么你也可以用 CF_OEMTEXT 来调用 GetClipboardData。同理,剪贴板也能把 CF_OEMTEXT 转换成 CF_TEXT。

        在 Windows NT 下,CF_UNICODETEXT、CF_TEXT 和 CF_OEMTEXT 之间可以互相转换。程序调用 SetClipboardData 时应该用它最方便的格式。同理,程序调用 GetClipboardData 时也应该用它何时使用的文本格式。如你所知,本书中的程序在有或没有 UNICODE 标识符的情况下都可以编译。如果你的程序是这样的,那么可能需要在调用 SetClipboardData 和 GetClipboardData 时实现这样的代码:如果定义了 UNICODE 标识符,则用 CF_UNICODETEXT;如果没有,就用 CF_TEXT。

        如下所示的 CLIPTEXT 程序演示了完成此任务的一种方法。

/*---------------------------------------------
	CLIPTEXT.C -- The Clipboard and Text
		    (c) Charles Petzold, 1998
----------------------------------------------*/

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

#ifdef UNICODE

#define CF_TCHAR CF_UNICODETEXT
TCHAR szDefaultText[] = TEXT("Default Text - Unicode Version");
TCHAR szCaption[] = TEXT("Clipboard Text Transfers - Unicode Version");
#else

#define CF_TCHAR CF_TEXT
TCHAR szDefaultText[] = TEXT(".Default Text - ANSI Version.");
TCHAR szCaption[] = TEXT(".Clipboard Text Transfers - ANSI Version.");

#endif

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
				PSTR szCmdLine, int iCmdShow)
{
	static TCHAR szAppName[] = TEXT("ClipText");
	HACCEL		 hAccel;
	HWND		 hwnd;
	MSG			 msg;
	WNDCLASS	 wndclass;

	wndclass.style				= CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc		= WndProc;
	wndclass.cbClsExtra			= 0;
	wndclass.cbWndExtra			= 0;
	wndclass.hInstance			= hInstance;
	wndclass.hIcon				= LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor			= LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground		= (HBRUSH)GetStockObject(WHITE_BRUSH);
	wndclass.lpszMenuName		= szAppName;
	wndclass.lpszClassName		= szAppName;

	if (!RegisterClass(&wndclass))
	{
		MessageBox(NULL, TEXT("This program requires Windows NT!"),
			szAppName, MB_ICONERROR);
		return 0;
	}

	hwnd = CreateWindow(szAppName, szCaption,
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL, NULL, hInstance, NULL);

	ShowWindow(hwnd, iCmdShow);
	UpdateWindow(hwnd);

	hAccel = LoadAccelerators(hInstance, szAppName);

	while (GetMessage(&msg, NULL, 0, 0))
	{
		if (!TranslateAccelerator(hwnd, hAccel, &msg))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}
	return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static PTSTR pText;
	BOOL		 bEnable;
	HGLOBAL		 hGlobal;
	HDC			 hdc;
	PTSTR		 pGlobal;
	PAINTSTRUCT  ps;
	RECT		 rect;

	switch (message)
	{
	case WM_CREATE:
		SendMessage(hwnd, WM_COMMAND, IDM_EDIT_RESET, 0);
		return 0;

	case WM_INITMENUPOPUP:
		EnableMenuItem((HMENU)wParam, IDM_EDIT_PASTE,
			IsClipboardFormatAvailable(CF_TCHAR) ? MF_ENABLED : MF_GRAYED);

		bEnable = pText ? MF_ENABLED : MF_GRAYED;

		EnableMenuItem((HMENU)wParam, IDM_EDIT_CUT, bEnable);
		EnableMenuItem((HMENU)wParam, IDM_EDIT_COPY, bEnable);
		EnableMenuItem((HMENU)wParam, IDM_EDIT_CLEAR, bEnable);
		break;

	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		case IDM_EDIT_PASTE:
			OpenClipboard(hwnd);

			if (hGlobal = GetClipboardData(CF_TCHAR))
			{
				pGlobal =(PTSTR) GlobalLock(hGlobal);
				if (pText)
				{
					free(pText);
					pText = NULL;
				}
				pText = (PTSTR) malloc(GlobalSize(hGlobal));
				lstrcpy(pText, pGlobal);
				InvalidateRect(hwnd, NULL, TRUE);
			}
			CloseClipboard();
			return 0;

		case IDM_EDIT_CUT:
		case IDM_EDIT_COPY:
			if (!pText)
				return 0;

			hGlobal = GlobalAlloc(GHND | GMEM_SHARE,
				(lstrlen(pText) + 1) * sizeof(TCHAR));

			pGlobal = (PTSTR)GlobalLock(hGlobal);
			lstrcpy(pGlobal, pText);
			GlobalUnlock(hGlobal);

			OpenClipboard(hwnd);
			EmptyClipboard();
			SetClipboardData(CF_TCHAR, hGlobal);
			CloseClipboard();

			if (LOWORD(wParam) == IDM_EDIT_COPY)
				return 0;
										// fall through fro IDM_EDIT_CUT

		case IDM_EDIT_CLEAR:
			if (pText)
			{
				free(pText);
				pText = NULL;
			}
			InvalidateRect(hwnd, NULL, TRUE);
			return 0;

		case IDM_EDIT_RESET:
			if (pText)
			{
				free(pText);
				pText = NULL;
			}

			pText = (PTSTR)malloc((lstrlen(szDefaultText) + 1) * sizeof(TCHAR));
			lstrcpy(pText, szDefaultText);
			InvalidateRect(hwnd, NULL, TRUE);
			return 0;
		}
		break;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		GetClientRect(hwnd, &rect);

		if (pText != NULL)
			DrawText(hdc, pText, -1, &rect, DT_EXPANDTABS | DT_WORDBREAK);
		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		if (pText)
			free(pText);
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}
CLIPTEXT.RC (excerpts)

// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

/////////////////////////////////////////////////////////////////////////////
//
// Menu
//

CLIPTEXT MENU
BEGIN
    POPUP "&Edit"
    BEGIN
        MENUITEM "Cu&t\tCtrl+X",                IDM_EDIT_CUT
        MENUITEM "&Copy\tCtrl+C",               IDM_EDIT_COPY
        MENUITEM "&Paste\tCtrl+V",              IDM_EDIT_PASTE
        MENUITEM "De&lete\tDel",                IDM_EDIT_CLEAR
        MENUITEM SEPARATOR
        MENUITEM "&Reset",                      IDM_EDIT_RESET
    END
END


/////////////////////////////////////////////////////////////////////////////
//
// Accelerator
//

CLIPTEXT ACCELERATORS
BEGIN
    "C",            IDM_EDIT_COPY,          VIRTKEY, CONTROL, NOINVERT
    "V",            IDM_EDIT_PASTE,         VIRTKEY, CONTROL, NOINVERT
    VK_DELETE,      IDM_EDIT_CLEAR,         VIRTKEY, NOINVERT
    "X",            IDM_EDIT_CUT,           VIRTKEY, CONTROL, NOINVERT
END
RESOURCE.H (excerpts)

// Microsoft Visual C++ 生成的包含文件。
// 供 ClipText.rc 使用
//
#define IDM_EDIT_CUT                    40001
#define IDM_EDIT_COPY                   40002
#define IDM_EDIT_PASTE                  40003
#define IDM_EDIT_CLEAR                  40004
#define IDM_EDIT_RESET                  40005

        这个程序的目的是让你在 Windows NT 下既可以运行 Unicode 版本的程序,也可以运行 ANSI 版本的程序,并且了解剪贴板是怎样在这两个字符集之间进行转换的。请注意 CLIPTEXT.C 开头的#ifdef 语句。如果定义了 UNICODE 标识符,CF_TCHAR(我指定的通用剪贴板文本格式名)等于 CF_UNICODE;如果没有定义 UNICODE,CF_TCHAR 就等于 CF_TEXT,在程序后面,IsClipboardFormatAvailable、GetClipboardData 和 SetClipboardData 函数都用 CF_TCHAR 来指定数据类型。

        在程序开始处(或无论何时选择 Edit 菜单上的 Reset 选项时),静态变量 pText 包含一个指向字符串的指针,这个字符串在 Unicode 程序版本中是 Unicode 字符串“Default Text - Unicode version”;在非 Unicode 程序版本中是字符串“Default Text - ANSI version”的字符串。可以用 Cut 或 Copy 命令把这个文本字符串传输到剪贴板里,也可以用 Cut 或 Delete 命令从程序中删除这个字符串。Paste 命令把剪贴板里的文本内容复制到 pText。在处理 WM_PAINT 消息时,pText 字符串被显示在程序的客户区。

        如果首先从 Unicode 程序上选择 Copy 命令,然后又从非 Unicode 程序上选择 Paste 命令,你就能看到文本从 Unicode 转换成了 ANSI。同样,如果操作反过来,文本也会从 ANSI 转换成 Unicode。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:90050次
    • 积分:2937
    • 等级:
    • 排名:第13308名
    • 原创:64篇
    • 转载:179篇
    • 译文:1篇
    • 评论:3条
    最新评论