windows 硬链接

先说下需求, 情况打开是, 开发的时候没什么问题, 但是要测试的时候可能要挪到另外一个目录去.

比如在某个目录下开发网站, 测试的时候不想使用 vhost 的话,就需要把文件挪到 www 目录下, 这可能让人有点不爽, 特别有些时候带着 svn, 挪过去就没有 svn 了, 整个copy 过去又很鸡肋 (svn中可能有别的不相干的东西, 比如策划的文档), 而且整个 copy 过去就变成了 两个 svn 目录, 这样要更新的时候要更新两个地方, 这样不说跟别人了, 可能自己跟自己的代码就会有冲突. 各种不方便.

对于这些情况, 硬连接就可以很方便的解决.
linux 下的硬连接是很常见的, windows 的话就不是很常见了.

mklink /d 链接目录 D:workspacetest

硬连接可以试文件也可以是目录, 创建硬连接可以很方便的是你的文件出现在想要的地方不用挪动源文件, 并且与快捷方式不同的是, 硬连接创建的文件被修改了也会同时修改源文件.

例子:

D:wampwww>mklink /d "D:wampwwwapi" "E:svntrunkweb-serverpublic"
symbolic link created for D:wampwwwapi <<===>> E:svntrunkweb-serverpublic

具体选项:

MKLINK [[/D] | [/H] | [/J]] Link Target
        /D      创建目录符号链接。默认为文件
                符号链接。
        /H      创建硬链接,而不是符号链接。
        /J      创建目录联接。
        Link    指定新的符号链接名称。
        Target  指定新链接引用的路径
                (相对或绝对)。

Windows GDI 教程(六) DC与加载位图

为了能够理清楚一些常见的 DC ,这里请让博主在介绍一下几种 DC 的获得方式:

  • BeginPaint 通过窗口 (hwnd) 获得
  • GetDC 通过窗口 (hwnd) 获得
  • CreatCompatibleDC 通过 hdc 获得

关于这三种情况获得的 DC,需要注意的是:从窗口上获得到的DC在释放资源的时候需要调用 ReleaseDC (释放引用,这样其他地方还可以接着用),而通过 CreatCompatibleDC 创建的 DC 我们通常称之为 Memory DC (博主管这叫“缓存DC”),当你不需要它的时候,需要通过 DeleteDC 来释放资源(因为这个DC是你单独申请的,如果你不用了,其他也不会有地方用它,只能直接 Delete)。

BeginPaint 与 GetDC

同样是通过窗口获得其客户端区域(Client area)的可绘制DC,BeginPaint 函数是先使窗口有效再重画,而 GetDC 没有这样,使用 GetDC 时由于窗口始终处于无效状态,不断会产生WM_PAINT消息。

BeginPaint 的表现是,收到 WM_PAINT 时绘制。所以正常情况下,如果没有手动触发 WM_PAINT 那么打开窗口则只绘制一次。

GetDC 的表现是,不断收到 WM_PAINT 消息,绘图区域(DC)不停地刷新。如果绘制过程比较慢的

CreatCompatibleDC

CreatCompatibleDC 函数的书面描述是,用来创建一个与指定设备兼容的内存设备上下文环境(DC)。

这个书面描述感觉有点不好理解,这里博主说一下自己的看法:

关于这个 CreatCompatibleDC 函数首先要了解这其中的单词:Creat、Compatible、DC。其中 Create 是“创建”、Compatible 是兼容的、DC 是博主说的“可画图的内存对象”。那么创建和 DC 都很好理解。这里我们重点要理解这个“兼容的”这个单词。

3个意思:

  1. 该DC的类型与兼容目标相同。虽然博主一再强调关于DC只需要把它当做“可以用来画图的内存就好了”,但是“橘生淮南则为橘,生于淮北则为枳”,从不同的设备中创建出来的DC,互相之间也是有差别的,由此产生的DC的类型包括:显示器、打印机、存储器和数据的索引。
  2. 该DC不需要指定区域、大小。感觉这一条有点凑数的感觉(博主:别打我!)
  3. 该DC与兼容的对象可以方便交流数据。该功能在GDI内部实现。比如使用 BitBlt 函数可以将兼容 DC 上的位图写入到目标 DC中。

加载BMP位图实例

BMP 是 Bitmap 的简写,中文常常称之为 “位图”。

#define WINDOW_WIDTH 400
#define WINDOW_HEIGHT 300

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HDC	mdc;
	HBITMAP hbmp; // 位图绘制对象句柄

	hdc = BeginPaint(hwnd, &ps); 

	// 创建缓存DC (当前窗口DC的兼容DC)
	mdc = CreateCompatibleDC(hdc);

	// 加载 "E:\bg.bmp" 到位图绘制对象 hbmp 中
	hbmp = (HBITMAP)LoadImage(
		NULL,			// 模块实例句柄(要加载的图片在其他DLL中时)
		"E:\bg.bmp",	// 位图路径
		IMAGE_BITMAP,	// 位图类型
		WINDOW_WIDTH,	// 指定图片宽
		WINDOW_HEIGHT,	// 指定图片高
		LR_LOADFROMFILE	// 从路径处加载图片
		);

	// 缓存DC选择位图绘制对象(可以理解为将图片存到mdc中)
	SelectObject(mdc, hbmp);

	// 将缓存DC中的位图复制到窗口DC上
	BitBlt(
		hdc,			// 目的DC
		0,0,			// 目的DC的 x,y 坐标
		WINDOW_WIDTH,	// 要粘贴的图片宽
		WINDOW_HEIGHT,	// 要粘贴的图片高
		mdc,			// 缓存DC
		0,0,			// 缓存DC的 x,y 坐标
		SRCCOPY 		// 粘贴方式
		);

	DeleteObject(hbmp);
	DeleteDC(mdc);
	EndPaint(hwnd, &ps);
}

效果图:
加载BMP图片

如何理解上述代码

对于初学者很容易对上述代码迷惑。常见的几个问题列举:

为什么要用 HBitMap 去 LoadImage,而不是直接用 DC 去 Load?

按照博主的思路来描述一下这个问题吧,画图可以简单的分成三个部分,分别是:
载体(如白纸、油画画板)、工具(如2B铅笔、马克笔)、颜料

对应到GDI程序中,使用画笔可以看成:载体(DC)、工具(画笔 HPEN)、颜料CreatePen 函数中传入)。

那么对应加载位图可以这样看:载体(DC)、工具(?)、颜料LoadImage 中指定图片)

这样很明显可以看到缺少一个工具加载位图的工具。也就是我们的 HBITMAP 对象。

这里拓展一下GDI中可以在DC用来绘制的工具:

对象 通过什么函数创建
Bitmap CreateBitmap, CreateBitmapIndirect, CreateCompatibleBitmap, CreateDIBitmap, CreateDIBSection
Bitmap对象只能被缓存DC(通过 CreatCompatibleDC创建)选中,并且单个bitmap不能同时被多个DC选中。
Brush CreateBrushIndirect, CreateDIBPatternBrush, CreateDIBPatternBrushPt, CreateHatchBrush, CreatePatternBrush, CreateSolidBrush
Font CreateFont, CreateFontIndirect
Pen CreatePen, CreatePenIndirect
Region CombineRgn, CreateEllipticRgn, CreateEllipticRgnIndirect, CreatePolygonRgn, CreateRectRgn, CreateRectRgnIndirect

此列表出处见 SelectObject function on MSDN

为什么要用 LoadImage 到 HBitMap,然后让 mdc 去 select,然后再让 mdc 复制到 hdc?

如果C语言基础学的好的话,那么这个问题也可以用一个很简单的角度去看待:
1.申请缓存空间
2.加载数据到缓存
3.输出数据(将缓存中的数据写入到目标内存)

以基础的C语言代码为例,假设是“输出用户输入的一个整数”,那么C语言代码如下:

// 1.(申请缓存空间) 声明一个4字节的int型变量,用于缓存用户输入
int num;
// 2.(加载数据到缓存) 将用户输入的数字加载到num中
scanf("%d", &num);
// 3.(将缓存中的数据写入到目标内存) 输出 num 到屏幕(将num的值写入到显存)
printf("你输入的是:%d n", num);

对应到加载图片的程序就可以这样理解了。

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HDC	mdc;
	HBITMAP hbmp;

	hdc = BeginPaint(hwnd, &ps); 

	// 1.(申请缓存空间) 创建缓存DC
	mdc = CreateCompatibleDC(hdc);

	// 2.(加载数据到缓存) 通过 Hbitmap 加载数据到 缓存DC
	hbmp = (HBITMAP)LoadImage( ... );
	SelectObject(mdc, hbmp);

	// 3.(将缓存中的数据写入到目标内存) 将缓存DC中的位图复制到窗口DC上
	BitBlt( ... );

	DeleteObject(hbmp);
	DeleteDC(mdc);
	EndPaint(hwnd, &ps);
}

这代码也就是看起来长了一点,没什么大不了的,本质上还是基础的程序逻辑。区别仅仅在于:
1.缓存的空间从 int 换成了 HDC
2.加载数据的方法从使用 scanf 换成了 LoadImageSelectObject
3.输出数据的对象从 int类型的num 写入到显存,换成了 HDC类型的mdc 写入到 hdc;输出数据的方法从 printf 换成了 BitBlt

PS:引用博主一句 装逼 的话来总结一下上面的对比:“程序做并且只在做两件事,一、读数据,二、写数据”

为什么不直接让 hdc 直接去 select 图片的 HBitMap?

1.首先上文有提到过 Bitmap 对象只能被缓存DC (Memory DC,通过 CreateCompatibleDC 创建的)选中。

2.其次,就算情况1不存在,bitmap 可以被 hdc 选中,这在程序的逻辑上也是一件很别扭的事情。依旧是套用 C语言基础的程序来看这个问题:

// 1.不申请缓存空间

// 2.3.直接获取数据并输出
printf("你输入的是:%d n", scanf( ? ) );

或者这样:

// 1.不申请缓存空间

// 2.3.直接获取数据并输出
printf("你输入的是:%s n", gets( ? ) );

基础好的可以马上发现上面两个示意的代码都是没法实现的。当然,如果你依旧觉得这个合理也是可以的,毕竟在其他的语言(比如python)里面这种情况是可以实现的。但是这样写的话,会有如下两种情况:

  1. 不自由。无法修改该数据,只能死板的输出。
  2. 效率低。没有缓存,要输出的时候必须要当时加载,下一次再次用到的时候又要重新加载。

综上。缓存DC是有存在的必要。

修改图片之后再显现的实例

为了照应上文,这里附上一个修改缓存DC中图片然后再显示到窗口的例子。

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HDC	mdc;
	HBITMAP hbmp; // 位图绘制对象句柄

	hdc = BeginPaint(hwnd, &ps); 

	// 加载图片到缓存 DC
	mdc = CreateCompatibleDC(hdc);
	hbmp = (HBITMAP)LoadImage(NULL, "E:\bg.bmp", IMAGE_BITMAP, WINDOW_WIDTH, WINDOW_HEIGHT, LR_LOADFROMFILE);
	SelectObject(mdc, hbmp);

	// 修改缓存DC (在缓存的图片上写字)
	TextOut(mdc, 0,0, TEXT("Hi, 缓存DC你好。"), strlen(TEXT("Hi, 缓存DC你好。")));

	// 将缓存DC中的位图复制到窗口DC上
	BitBlt(hdc, 0,0, WINDOW_WIDTH, WINDOW_HEIGHT, mdc, 0,0, SRCCOPY);

	DeleteObject(hbmp);
	DeleteDC(mdc);
	EndPaint(hwnd, &ps); 
}

效果图:
修改缓存DC

教程索引

Windows GDI 教程(五) 填充图形和画刷

填充图形(Filled Shape)是指封闭状态的图形,常见有的矩形、多边形、圆形、椭圆等等。而画刷(Brush)与画笔(Pen)不同,画笔是专门用于绘制线条,而画刷则专门用于填充如矩形、椭圆形等填充图形。

在 DC 中,画刷与画笔一样,都是使用 SelectObject 函数来选中。并且画刷与画笔可以同时存在,互相并不冲突。

Rectangle

本节的重点是矩形(Rectangle)的绘制,而绘制矩形的函数名也就是英文的单词 Rectangle 。该函数用于在目标DC中绘制矩形的函数,只需要传一个DC的句柄以及矩形的左上点坐标和右下点坐标。

BOOL Rectangle(
  _In_  HDC hdc,		// 目标DC句柄
  _In_  int nLeftRect,	// 左
  _In_  int nTopRect,	// 上
  _In_  int nRightRect,	// 右
  _In_  int nBottomRect	// 下
);

绘制空心矩形

#define RED_COLOR RGB(255,0,0)

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HPEN hpen;
	int i;

	// 创建并选择画笔
	hdc = BeginPaint(hwnd, &ps);
	hpen = CreatePen(PS_SOLID, 1, RED_COLOR);
	SelectObject(hdc,hpen);

	// 绘制矩形
	Rectangle(hdc,80, 50, 300, 200);

	// 清理资源
	DeleteObject(hpen);
	EndPaint(hwnd, &ps);
}

效果图:
绘制空心矩形

PS:如果没有用 SelectObject 指定画笔的话,会使用默认画笔(样式:soild、宽度:1px、黑色)

CreateSolidBrush

创建一个实心填充的画刷。

HBRUSH CreateSolidBrush(
  _In_  COLORREF crColor // 指定画刷颜色
);

绘制实心矩形


#define RED_COLOR RGB(255,0,0)
#define BLUE_COLOR RGB(0,0,255)

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HBRUSH hbrush;
	HPEN hpen;

	hdc = BeginPaint(hwnd, &ps);

	hpen = CreatePen(PS_SOLID, 10, RED_COLOR);	// 创建红色实心10px粗的画笔
	hbrush = CreateSolidBrush(BLUE_COLOR); // 创建蓝色画刷

	SelectObject(hdc, hpen);	// 指定画笔
	SelectObject(hdc, hbrush);	// 指定画刷

	// 绘制矩形
	Rectangle(hdc,80, 50, 300, 200);

	// 清理资源
	DeleteObject(hpen);
	DeleteObject(hbrush);
	EndPaint(hwnd, &ps);
}

效果图:
绘制实心矩形

填充图形列表

本节并不专门讲解除了矩形以外的其他图形。因此,以下简单的将绘制填充图形的所有的函数列举出来:

  • Rectangle 绘制一个矩形。
  • RoundRect 绘制一个圆角矩形。
  • Ellipse 绘制一个椭圆形。
  • Pie 绘制一个圆饼扇形。
  • Chord 绘制一个切割椭圆。
  • Polygon 绘制多边形。
  • PolyPolygon 绘制同时绘制多个封闭的多边形。
  • FillRect 使用画刷填充一块矩形区域。
  • FrameRect 使用画刷绘制一块矩形区域的边框。
  • InvertRect 将制定的矩形范围内的像素反色。

更多信息详见: Filled Shape Functions on MSDN

CreateHatchBrush

创建影线画刷。

HBRUSH CreateHatchBrush(
  _In_  int fnStyle,		// 影线样式
  _In_  COLORREF clrref 	// 画刷颜色
);

绘制椭圆、圆角矩形、扇形以及切割椭圆实例

// 画刷样式
int ibrushStyle[4] = {
	HS_VERTICAL,	/* ||||| 垂直 */
	HS_HORIZONTAL,	/* ----- 水平 */
	HS_CROSS,		/* +++++ 正十字 */
	HS_DIAGCROSS	/* xxxxx 斜十字 */
};

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HPEN hPen;
	HBRUSH hBrush[4];
	int i;

	hdc = BeginPaint(hwnd, &ps);

	// 初始化4把画刷
	for(i = 0; i < 4; i++)
		hBrush[i] = CreateHatchBrush(ibrushStyle[i], RGB(0,0,255));

	// 画椭圆
	SelectObject(hdc, hBrush[0]);
	Ellipse(hdc, 0,0, 140,100);
	TextOut(hdc,55,100,"椭圆",strlen("椭圆"));

	// 画圆角矩形
	SelectObject(hdc,hBrush[1]);
	RoundRect(hdc, 160,0,  320,100, 30,30);
	TextOut(hdc, 210,100, "圆角矩形",strlen("圆角矩形"));

	// 画扇形
	SelectObject(hdc,hBrush[2]);
	Pie(hdc, 0,130, 140,230, 50,50, 300,300);
	TextOut(hdc,55,230,"扇形",strlen("扇形"));

	// 画切割椭圆(Chord)
	SelectObject(hdc,hBrush[3]);
	Chord(hdc, 160,130, 320,230, 50,50, 600,300);
	TextOut(hdc,210,230,"Chord",strlen("Chord"));

	// 清理资源
	for(i = 0; i < 4; i++)
		 DeleteObject(hBrush[i]);
	EndPaint(hwnd, &ps);
}

效果图:
绘制填充图形

PS:如果绘制椭圆的时候,设置其区域为正方形,则画出来的就是圆形。扇形也同样如此,画出来的就是圆的扇形。

画刷类型列表

这边再给大家拓展一下所有画刷的列表,虽然教程内没有专门讲解,但是说不定其中就有你心动的功能:

  • CreateSolidBrush 创建一个实心颜色(Solid color)画刷
  • CreateHatchBrush 创建一个影线模式(Hatch pattern)的画刷
  • CreatePatternBrush 创建一个位图模式(Bitmap pattern)的画刷
  • CreateBrushIndirect 根据指定的样式、颜色、模式(pattern)来创建一个画刷
  • CreateDIBPatternBrushPt 根据 DIB 模式(pattern from DIB)创建一个画刷

其他更多操作画刷的函数详见 Brush Functions on MSDN

教程索引

上一讲:Windows GDI 教程(四) 文字与字体
下一讲:Windows GDI 教程(六) DC与加载位图

Windows GDI 教程(四) 文字与字体

TextOut

在目标DC的指定坐标处添加一行文字。

BOOL TextOut(
  _In_  HDC hdc,			// DC句柄
  _In_  int nXStart,		// x坐标
  _In_  int nYStart,		// y坐标
  _In_  LPCTSTR lpString,	// 文字内容
  _In_  int cchString		// 文字长度
);

输出文字实例

void Paint(HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hdc;

	hdc = BeginPaint(hwnd, &ps); 
	
	// 设置文字背景色
	SetBkColor(hdc, RGB(0,0,0));
	// 设置文字颜色
	SetTextColor(hdc, RGB(255,255,255));
	// 输出文字 Helloworld
	TextOut(hdc, 50, 50, TEXT("Hello world"), sizeof(TEXT("Hello world")));

	EndPaint(hwnd, &ps); 
}

效果图:
输出文字

选择字体实例

/*
 * 选择字体
 */
HFONT ChooseMyFont() 
{ 
	CHOOSEFONT cf; 
	LOGFONT lf; 
	HFONT hfont; 

	// CHOOSEFONT 结构 
	cf.lStructSize = sizeof(CHOOSEFONT); 
	cf.hwndOwner = (HWND)NULL; 
	cf.hDC = (HDC)NULL; 
	cf.lpLogFont = &lf; 
	cf.iPointSize = 0; 
	cf.Flags = CF_SCREENFONTS; 
	cf.rgbColors = RGB(0,0,0); 
	cf.lCustData = 0L; 
	cf.lpfnHook = (LPCFHOOKPROC)NULL; 
	cf.lpTemplateName = (LPSTR)NULL; 
	cf.hInstance = (HINSTANCE) NULL; 
	cf.lpszStyle = (LPSTR)NULL; 
	cf.nFontType = SCREEN_FONTTYPE; 
	cf.nSizeMin = 0; 
	cf.nSizeMax = 0; 

	// 选择字体对话框 
	ChooseFont(&cf); 
	// 得到HFONT 返回
	hfont = CreateFontIndirect(cf.lpLogFont); 
	return (hfont); 
}

void Paint(HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hdc;
	HFONT hFontSetting;

	hdc = BeginPaint(hwnd, &ps); 

	// 获得字体对象句柄
	hFontSetting = ChooseMyFont();
	// DC 加载字体
	SelectObject(hdc, hFontSetting);

	// 同样输出文字 Helloworld
	TextOut(hdc, 0, 0, TEXT("Hello world"), sizeof(TEXT("Hello world")));

	EndPaint(hwnd, &ps); 
}

效果图:
选择字体
字体+大小

设置排版实例

除了 TextOut 之外,DrawText也可以用于输出文字,他比 TextOut 要多出来的地方就是可以指定文字所在的区域,然后设置文字在该区域内的排版

void Paint(HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hdc;
	RECT rc;

	hdc = BeginPaint(hwnd, &ps); 

	// 获取当前窗口的矩形区域
	GetClientRect(hwnd, &rc); // 实际上就是获取左上和右下的两个坐标
	
	DrawText(
		hdc,
		TEXT("Hello worldnHello Windows GDI"),	// 文字内容
		strlen(TEXT("Hello worldnHello Windows GDI")), // 文字长度
		&rc,		// 输出区域
		DT_CENTER	// 排版格式 居中
	);

	EndPaint(hwnd, &ps); 
}

效果图:
drawtext

教程索引

上一节:Windows GDI 教程(三) 绘制函数图像以及多边形
下一节:Windows GDI 教程(五) 填充图形和画刷

Windows GDI 教程(三) 绘制函数图像以及多边形

SetPixel

画一个点。函数原型:

COLORREF SetPixel(
  _In_  HDC hdc,	// DC句柄
  _In_  int X,		// x 坐标
  _In_  int Y,		// y 坐标
  _In_  COLORREF crColor // 颜色
);

绘制函数图像

上一节有说过在画渐变的时候其实用画点的函数会更好用,这里就使用 SetPixel 函数来绘制一个点。

#include <windows.h>
#include <math.h>

// 用于注册的窗口类名
const char g_szClassName[] = "myWindowClass";

#define POS_X 150		// 坐标系原点 x 
#define POS_Y 130		// 坐标系原点 y 
#define POS_WIDTH 100	// 坐标系宽度
#define BLACK_COLOR RGB(0, 0, 0) // 黑色

/*
 * 要绘制的函数:
 * f(x) = sin(x/10) * 20
 */
double f(double x)
{
	return sin(x/10) * 20;
}

void Paint(HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hdc;   // DC(可画图的内存对象) 的句柄
    HPEN hpen; // 画笔
    int x, y;
	double i;

    hdc = BeginPaint(hwnd, &ps);
    hpen = CreatePen(PS_SOLID, 1, BLACK_COLOR);
    SelectObject(hdc, hpen);

	// 绘制 x 轴
    MoveToEx(hdc, POS_X - POS_WIDTH, POS_Y, NULL);
    LineTo(hdc, POS_X + POS_WIDTH, POS_Y);

	// 绘制 y 轴
	MoveToEx(hdc, POS_X, POS_Y + POS_WIDTH, NULL);
	LineTo(hdc, POS_X, POS_Y - POS_WIDTH);

	// 绘制 f(x) = sin(x/10) * 20 的函数图像
	for (i = -POS_WIDTH; i < POS_WIDTH; i += 0.05) // x每增长0.05绘制1个点
	{
		x = POS_X + i;
		y = POS_Y + f(i);
		// 在 (x,y) 处绘制一个点
		SetPixel(hdc, x, y, BLACK_COLOR);
	}

    DeleteObject(hpen);
    EndPaint(hwnd, &ps);
}

效果图:
绘制函数图像

PS: 这个画点的 SetPixel 函数很简单,各位可以试着用它来修改一下上一节中颜色渐变的例子。

Polyline

Polyline 与 lineto 类似,区别在于 lineto 每画一条线当时就写入显存,而 Polyline则是把指定的坐标数组中所有线都画完之后才写入显存。

BOOL Polyline(
  _In_  HDC hdc,			// DC句柄
  _In_  const POINT *lppt,	// 要绘制的点的数组
  _In_  int cPoints			// 要绘制的点的个数
);

一次绘制多条连通的线示例

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HPEN hPen;
	POINT poly[4];

	hdc = BeginPaint(hwnd, &ps);

	// 设置画笔
	hPen = CreatePen(PS_SOLID, 3, RGB(0,0,255));
	SelectObject(hdc, hPen);

	poly[0].x = 100;
	poly[0].y = 150;

	poly[1].x = 200;
	poly[1].y = 150;

	poly[2].x = 150;
	poly[2].y = 50;

	poly[3].x = 250;
	poly[3].y = 50;

	Polyline(hdc, poly, 4);

	EndPaint(hwnd, &ps);
}

效果图:
绘制不规则图形

RECT 结构体

RECT 即 Rectangle 矩形结构体,实际上就存储了两个坐标,一个是左上、一个是右下。

typedef struct 
{
    LONG    left;	// 左
    LONG    top;	// 上
    LONG    right;	// 右
    LONG    bottom; // 下
} RECT;

SetRect

用于设置 RECT 结构体的值。

BOOL SetRect(
    __out LPRECT lprc,	// RECT 结构体指针
    __in int xLeft,		// 左
    __in int yTop,		// 上
    __in int xRight,	// 右
    __in int yBottom	// 下
);

RectVisible

检查目标DC中的指定矩形区域是否可见

WINGDIAPI BOOL WINAPI RectVisible(
	__in HDC hdc, 				// 目标DC
	__in CONST RECT * lprect 	// 指定矩形区域
);

SetViewportOrgEx

修改指定DC中的坐标系原点

BOOL SetViewportOrgEx(
  _In_   HDC hdc,
  _In_   int X,
  _In_   int Y,
  _Out_  LPPOINT lpPoint
);

多边形绘制实例

包括三角形、正方形、五边形、六边形。

POINT aptTriangle[4] = { {50,2}, {98,86},  {2,86}, {50,2}}, 
	aptRectangle[5]  = { { 2,2}, {98,2},  {98,98},  {2,98}, {2,2}}, 
	aptPentagon[6]   = { {50,2}, {98,35}, {79,90}, {21,90}, {2,35}, {50,2}}, 
	aptHexagon[7]    = { {50,2}, {93,25}, {93,75}, {50,98}, {7,75}, {7,25}, {50,2}}; 

void Paint(HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hdc;   // DC(可画图的内存对象) 的句柄
    HPEN hpen; // 画笔
	RECT rc;

	hdc = BeginPaint(hwnd, &ps); 
	// 初始化矩形 rc 用于表明绘图范围
	SetRect(&rc, 0, 0, 100, 100); 

	// 如果 rc 所包括的范围可见
	if (RectVisible(hdc, &rc)) 
		// 绘制三角形
		Polyline(hdc, aptTriangle, 4); 

	// 修改当前DC坐标系原点为 (100,0)
	SetViewportOrgEx(hdc, 100, 0, NULL); 
	if (RectVisible(hdc, &rc)) 
		// 绘制正方体
		Polyline(hdc, aptRectangle, 5); 

	// 修改当前DC坐标系原点为 (0,100)
	SetViewportOrgEx(hdc, 0, 100, NULL); 
	if (RectVisible(hdc, &rc)) 
		// 绘制五边形
		Polyline(hdc, aptPentagon, 6); 

	// 修改当前DC坐标系原点为 (100,100)
	SetViewportOrgEx(hdc, 100, 100, NULL); 
	if (RectVisible(hdc, &rc)) 
		// 绘制六边形
		Polyline(hdc, aptHexagon, 7); 

	EndPaint(hwnd, &ps); 
}

效果图:
绘制多边形

教程索引

上一节:Windows GDI 教程(二) 画笔的颜色与样式
下一节:Windows GDI 教程(四) 文字与字体

Windows GDI 教程(二) 画笔的颜色与样式

以上一节的代码为蓝本,我们所需要关注的仅仅是 Paint 函数内的内容。首先我们来补充说明一下上一讲代码中没有详细说明的问题。

// 创建画笔
hpen = CreatePen(PS_SOLID, 1, RGB(255,0,0));

这里通过 CreatePen 创建了一个画笔。其中:

  • 参数一 PS_SOLID 是指画笔的样式,具体的样式形态会在本文下方详述。
  • 参数二 1 是指明该画笔的宽度即 1px。需要注意的是 px 是像素单位,1px 也可以简单的理解为一个点。由于概念简单这里不做过多赘述,更多的具体信息可以去百度百科。
  • 参数三 RGB(255,0,0) 则是指定画笔的颜色了。具体描述见下文。

RGB 颜色

RGB色彩模式是工业界的一种颜色标准,通过红(Red)、绿(Green)、蓝(Blue)三种颜色搭配可以显示出非常丰富的色彩来。目前我们程序中看到的这种 RGB 其实也可以简单的当做 256 色(0~255)来看。总共能搭配出来的颜色有 256*256*256 = 16777216 种。

我们用最简单的方式来看到这个 RGB 颜色,当其值为 0 的时候,就没有该颜色,当其值为 255 的时候即纯色。这样每个颜色有 2 种状态来看的话,可以搭配出 8 个颜色,如下图:

对应数据如下表格:

RGB描述 绿 十六进制 颜色
RGB(0,0,0) 0 0 0 000000 黑色
RGB(255,0,0) 255 0 0 FF0000 红色
RGB(0,255,0) 0 255 0 00FF00 绿色
RGB(0,0,255) 0 0 255 0000FF 蓝色
RGB(255,255,0) 255 255 0 FFFF00 黄色
RGB(255,0,255) 255 0 255 FF00FF 紫色
RGB(0,255,255) 0 255 255 00FFFF 兰色
RGB(255,255,255) 255 255 255 FFFFFF 白色

如果把每种颜色的状态由浅入深的分为12个状态的话,调配的颜色可以参考下图:
0dd7912397dda1448aa71c91b2b7d0a20cf48662

绘制不同颜色的线条

下面的情况就是每一列画上蓝色从0~255的渐变。然后每一行配上红色从0~255的渐变。这中间其实每一行都画的是一个长度 256px 高度2px 的矩形。

void Paint(HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hdc;   // DC(可画图的内存对象) 的句柄
    HPEN hpen; // 画笔
    int red, blue; // 红、绿两色
    int y = 1, x = 1; // 行、列

    // 通过窗口句柄获取该窗口的 DC
    hdc = BeginPaint(hwnd, &ps);

    for (red = 0; red < 256; red++)
    {
        for (blue = 0; blue < 256; blue++)
        {
			// 创建画笔
            hpen = CreatePen(PS_SOLID, 1, RGB(red, 0, blue));
			// 选中画笔
            SelectObject(hdc, hpen);

			// 画一条从 (x, y) 到 (x, y+1) 的垂直的线
            MoveToEx(hdc, x, y, NULL);
            LineTo(hdc, x++, y + 1);

			// 删除画笔
            DeleteObject(hpen);
        }
		// 一行画完 行+1, 列重新置 1
		y += 1;
        x = 1;
    }

    // 关闭DC 结束绘制
    EndPaint(hwnd, &ps);
}

效果如下:
使用线条绘制一个简单的颜色渐变

PS:画完一行之后 y 坐标仅仅只是加1,所以会覆盖上一行的下半部分。其实做这个效果用画点的函数效果会更好,不过为了让大家更容易上手所以这里还是继续使用 LineTo。

画笔的不同样式

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HPEN hpen;
	int i;

	// 画笔类型
	int Penstyle[] = {
		PS_SOLID, 		/*   直线   */
		PS_DASH, 		/* -------  */
		PS_DOT, 		/* .......  */
		PS_DASHDOT, 	/* _._._._  */
		PS_DASHDOTDOT, 	/* _.._.._  */
		PS_NULL  		/*          */
	};

	hdc = BeginPaint(hwnd, &ps);

	for(i=0; i < sizeof(Penstyle) / sizeof(Penstyle[0]); i++)
	{
		// 创建画笔
		hpen = CreatePen(Penstyle[i], 1, RGB(0,0,0));
		// 设备选择画笔
		SelectObject(hdc,hpen);

		// 画一条从(30*i+80, 50) 到 (30*i+80, 200) 的线
		MoveToEx(hdc, 30 * i + 80, 50, NULL);
		LineTo(hdc, 30 * i + 80, 200);
	}

	DeleteObject(hpen);
	EndPaint(hwnd, &ps);
}

效果图:
画笔的六种样式

使用 Lineto 绘制一个简单的三角形

lineto 是可以一直连续使用的。

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HPEN hpen;

	hdc = BeginPaint(hwnd, &ps);

	// 创建画笔
	hpen = CreatePen(PS_SOLID, 1, RGB(0,0,0));
	// 设备选择画笔
	SelectObject(hdc,hpen);

	// 画一条从(30*i+80, 50) 到 (30*i+80, 200) 的线
	MoveToEx(hdc,100, 200, NULL);
	LineTo(hdc, 300, 200);
	LineTo(hdc, 200, 50);
	LineTo(hdc,100, 200);

	DeleteObject(hpen);
	EndPaint(hwnd, &ps);
}

效果图:
Lineto绘制三角形

关于画笔的其他注意点

实际上创建画笔除了 CreatePen 函数 之外还有一个 ExtCreatePen 函数。通过 CreatePen 创建的画笔我们一般称为装饰画笔(Cosmetic Pens),通过 ExtCreatePen 创建的我们一般称之为几何画笔(Geometric Pens)。

装饰画笔:简单、轻便。绘制速度快。不收坐标空间变化影响,所以绘制的线条宽度都是相等。可以设置的属性只有:样式、宽度、颜色。
几何画笔:拓展了更多的功能,绘制速度较慢,收到坐标空间变化的影响,具有多种属性。几何画笔除了包括装饰画笔的三种属性之外,还多了模式(Pattern)、影线(Hatch)、线端(End Cap)、结合方式(jion)等属性。具有更丰富的表现力。不过为了让大家能很容易的学下去我们还是先不讨论几何画笔。

教程索引

上一节:Windows GDI 教程(一) 一个简单的绘图程序
下一节:Windows GDI 教程(三) 绘制函数图像以及多边形

Windows GDI 教程(一) 一个简单的绘图程序

常见的图形编程库,除了 GDI 外还有 GDI+、OpenGL、DirectX等等,GDI 是其中最基础的一个库。所以 GDI 注定了不会有高级应用,有兴趣的就当刷低级怪吧。

在教程的最开始,需要简单的说明一些前置条件。

开发环境与前言

首先是标明开发环境:
操作系统:win7 (xp应该可以,win8未测试)
使用工具:visual studio 2010(或更高)

窗口创建

以前代码的前置问题,首先本教程内的 GDI 画图,在最开始部分主要是在窗口内部绘制(为避免混乱窗口外部,也就是整个桌面的绘制会在很后面的地方讨论)。因此,这里需要对于创建窗口一定的了解。为了让大家可以直接复制完代码就可以在一个文件里面运行,博主准备的代码是手动动态创建窗口的代码,所以这里创建窗口的代码有点长,不过大家不要怕,我们要关注的只是中间的一小部分。这里博主先把代码贴上:

#include <windows.h>
 
// 用于注册的窗口类名
const char g_szClassName[] = "myWindowClass";
 
/*
 * 第四步,窗口过程
 */
LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        // 窗口绘制消息
        case WM_PAINT:
            /*
             * 我们只需要在这里调用我们的 GDI 绘制函数就可以,其他地方可以先无视
             */
        break;
        // 窗口关闭消息
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        // 窗口销毁消息
        case WM_DESTROY:
            PostQuitMessage(0); // 发送离开消息给系统
        break;
        // 其他消息
        default:
            // pass 给系统,咱不管
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}
 
/*
 * 第一步,注册窗口类
 */
void RegisterMyWindow(HINSTANCE hInstance)
{
    WNDCLASSEX wc;  
 
    // 1)配置窗口属性
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = 0;
    wc.lpfnWndProc   = MyWindowProc; // 设置第四步的窗口过程回调函数
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = g_szClassName;
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);
 
    // 2)注册
    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
        exit(0); // 进程退出
    }
}
 
/*
 * 第二步,创建窗口
 */
HWND CreateMyWindow(HINSTANCE hInstance, int nCmdShow)
{
    HWND hwnd;
    hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        g_szClassName,
        TEXT("我的窗口名称"),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, // 出现坐标 x,y 默认分配 窗口宽 400 高 300
        NULL, NULL, hInstance, NULL);
 
    if(hwnd == NULL)
    {
        MessageBox(NULL, TEXT("窗口创建失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
        exit(0); // 进程退出
    }
 
    // 显示窗口
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);
 
    return hwnd;
}
 
/*
 * 主函数
 */
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    HWND hwnd;
    MSG Msg;
 
    // 第一步,注册窗口类
    RegisterMyWindow(hInstance);
    // 第二步:创建窗口
    hwnd =  CreateMyWindow(hInstance, nCmdShow);
    // 第三步:消息循环
    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

运行效果图:
创建一个空白窗口

这个创建窗口的代码很长,看起来有点吓人,但是学习 GDI 的过程中,这其中几乎是完全不需要记忆的,只要有一定的了解,然后会 copy 就可以了。当然如果你能懂也是更好,以上代码的出处为 《Windows SDK 教程(三) 一些细节以及动态创建控件》,有兴趣的可以去看看。

那么博主也说过了,最开始的时候,这段长长的代码其实注意一个地方就可以了,就是其中的第四步窗口过程中的一个小 case。

/*
 * 第四步,窗口过程
 */
LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
		// 窗口绘制消息
		case WM_PAINT:
			/*
			 * 只有这一个 case 是我们 GDI 入门中需要注意的
			 *
			 * 当程序执行到这个地方的时候,意味着系统像我们的程序发送了 WM_PAINT 消息
			 * 也就是告诉我们的程序,可以开始绘制窗口的内容了。
			 *
			 */
		break;
		// 其余略...
    }
    return 0;
}

这样看来,貌似我们要注意的地方确实很小吧。那么我接着往下走。

PS:默认情况下,系统只会向我们的程序发送一次 WM_PAINT 消息。如果想要再来一次,需要使用 SendMessage 函数,来自己向自己手动发送该消息。

坐标系

GDI 的绘图坐标系与普通的数学坐标系不同,原点 (0,0) 位于左上角。如图:
坐标系

设备上下文(DC)

DC (设备上下文, Device Contexts)是 GDI 编程中一个很基础同时也很重要的概念。博主以前看过不少网上的资料以及书上的描述,总感觉他们说的都很奇怪。这里博主为了方便大家理解就说说自己的看法:

大家只要把 DC 当成一个保存图像的内存对象即可。当我们使用 GDI 提供的函数去操作 DC 的时候,也就意味着在使用函数去修改保存在这块内存上的图像。

BeginPaint 与 EndPaint

用于从目标窗口获取可画图的 DC,以及关闭这个 DC。

函数原型

HDC BeginPaint(
  _In_   HWND hwnd,   			// 传入想要获取 DC 的窗口句柄
  _Out_  LPPAINTSTRUCT lpPaint  // 保存目标窗口的绘图信息
);


BOOL EndPaint(
  _In_  HWND hWnd,					// 目标窗口的句柄
  _In_  const PAINTSTRUCT *lpPaint  // 目标窗口的绘图信息
);

SelectObject

设置目标 DC 选中指定的对象(如画笔、画刷、图片等等)。

函数原型

HGDIOBJ SelectObject(
  _In_  HDC hdc,        // 目标 DC 的句柄
  _In_  HGDIOBJ hgdiobj // 被选中的对象
);

CreatePen

创建一个画笔(pen)对象。

函数原型

HPEN CreatePen(
  _In_  int fnPenStyle,		// 样式
  _In_  int nWidth,			// 宽度
  _In_  COLORREF crColor	// 颜色
);

MoveToEx

移动绘制的初始位置。未移动则默认是 (0,0)。(C语言基础好的可以联想 fseek 函数)

函数原型

BOOL MoveToEx(
  _In_   HDC hdc,			// 操作目标DC的句柄
  _In_   int X,				// x 坐标
  _In_   int Y,				// y 坐标
  _Out_  LPPOINT lpPoint 	// 保存移动后的当前坐标
);

LineTo

使用当前选中的对象(selected object、通常是画笔)从当前位置绘制一条直线到目标位置。

函数原型

BOOL LineTo(
  _In_  HDC hdc,	// 目标DC句柄
  _In_  int nXEnd,	// 目标位置 x 坐标
  _In_  int nYEnd	// 目标位置 y 坐标
);

绘制直线实例

#include <windows.h>

// 用于注册的窗口类名
const char g_szClassName[] = "myWindowClass";

void Paint(HWND hwnd) 
{
	// paint struct 绘图结构体,存储目标窗口可以绘图的客户端区域(client area)
	PAINTSTRUCT ps;
	HDC hdc;   // DC(可画图的内存对象) 的句柄
	HPEN hpen; // 画笔

	// 通过窗口句柄获取该窗口的 DC
	hdc = BeginPaint(hwnd, &ps);
	// 创建画笔
	hpen = CreatePen(PS_SOLID, 1, RGB(255,0,0));
	// DC 选择画笔
	SelectObject(hdc,hpen);
	// (画笔)从初始点移动到 50,50
	MoveToEx(hdc, 50, 50, NULL);
	// (画笔)从初始点画线到 100,100
	LineTo(hdc, 150, 100);

	EndPaint(hwnd, &ps);
}

/*
 * 第四步,窗口过程
 */
LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
		// 窗口绘制消息
		case WM_PAINT:
			Paint(hwnd); // 调用我们的 GDI 绘制函数
		break;
		// 其余略...
    }
    return 0;
}

// 其余略

运行效果图:
draw_line

function show() {
var code = document.getElementById(‘whole_code’);
if (code.style.display) {
code.style.display = “”;
} else {
code.style.display = “none”;
}
}

点击查看完整代码

教程索引

下一节:Windows GDI 教程(二) 画笔的颜色与样式