(25):监视剪贴板
最后更新于:2022-04-01 20:08:00
自从郭大侠和蓉儿离开桃花岛后,最近岛比较寂静,有一种“门前冷落鞍马稀”的感觉。于是,老邪就拿出《九阴真经》认真阅读,同时用迅雷下载经典大剧《汉武大帝》晚上睡觉前看上几集,老邪一向是善于向古圣先贤学习。
大家知道,迅雷有一个监视剪贴板的功能,如果发现你复制剪贴板的内容包含符合迅雷下载地址格式的就弹出新建下载任务窗口。
可能我们也希望在自己的应用程序中也加入这样XX的功能,其实不难,真的,我用老顽童的人格保证,实现该功能是灰常简单的。
我们需要完成两步就行了。
1、调用AddClipboardFormatListener函数,指定哪个窗口有资格去捕捉剪贴板内容更新的消息。
2、处理WM_CLIPBOARDUPDATE消息,当剪贴板的内容发生变化后,消息处理程序会收到这条消息通知。
我相信,你一定想到了实现思路了。呵呵,我一向对于大家的领悟能力是很有信心的,所以,有时候我故意不说那么明白。其实我们学编程,不能什么问题都指望别人帮忙解决,依靠自己去解决问题才是上尚之道。
今天老邪不用C++来做这个例子,用什么?VB,呵呵,其实也可以,只是许多自负的人看不起VB,很可悲;Delphi?还是不要,不然老顽童会骂人。还是用C#,一来它的语法符合C风格,二来它也可以调用Win32 API。
估计有人会纳闷了,写Win32程序不是要用C++吗?干吗用C#?呵呵,很简单,因为我是老邪,本来俺练的盖世武功就是“旁门左道”,更何况,这年头,人是以非主流为无上光荣的。今天老邪也想“光荣”一回,免得蓉儿说我老了,功夫不行了。
而且,我咨询过相关律师,法律上并没有规定写Win32程序一定要用C++,再说,WinForm本质上也是Win32,而且人家WinForm还可以做Win64呢。
正因为今天的任务很简单,所以我忍不住拿出了看家本领——吹牛,是啊,吹牛好啊,人生难得几回吹。
在处理WM_CLIPBOARDUPDATE消息之前,一定要先调用AddClipboardFormatListener函数,它的原型如下:
~~~
/* c++ */
BOOL WINAPI AddClipboardFormatListener(
_In_ HWND hwnd
);
~~~
参数有且只有一个,就是窗口的句柄,你想让哪个窗口监控剪贴板,你就把这个窗口的句柄传进去。
下面是DllImport
~~~
// C#
[DllImport("User32.dll")]
public extern static bool AddClipboardFormatListener(IntPtr hwnd);
~~~
然后,在Form的构造函数中调用。
~~~
public Form1()
{
InitializeComponent();
AddClipboardFormatListener(this.Handle);
}
~~~
接着,如何写消息处理过程呢?重写WndProc方法。
~~~
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_CLIPBOARDUPDATE)
{
if (Clipboard.ContainsText())
{
string strdata = Clipboard.GetText();
if (strdata.StartsWith("http://") || strdata.StartsWith("https://"))
{
this.txtOutput.AppendText(string.Format("在{0}剪贴板发生了变化。\r\n" +
"包含URI:{1}" +
"\r\n---------------------------------------------\r\n",
DateTime.Now.ToString("HH:mm:ss"),
strdata));
// 返回0表示已经处理消息
m.Result = new IntPtr(0);
}
}
}
else
{
base.WndProc(ref m);
}
}
~~~
最后,可以测试,随便复制一些URI到剪贴板。如下图所示。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd31425d48.jpg)
好了,大功告成,继续下电影去,拜拜。
';
(24):计时器
最后更新于:2022-04-01 20:07:58
有好些时间没写博客了,今天要来耍一下计时器,就是我们常说的Timer,它虽然不是什么复杂的东西,也称不 上牛X,不过,用处还是不少的,对于那些需要每隔一定时间执行一次的任务,那是相当有用。
先来认识一下一对函数,注意,是一对,不是一个。
SetTimer——设置并启用计时器;
KillTimer——取消计时器。
现在你明白为什么要一对的原因了,就好比进程操作,有启动或创建进程的函数,就肯定要有关闭进程的函数;有GetDC就肯定要伴随着ReleaseDC函数。阴与阳是此消彼长的。
先说SetTimer,函数的定义我不说了,自己看头文件和MSDN就行了,主要说说以下两个参数:
nIDEvent指的是计时器的ID,一个数值,你可以随例取,只要不是负数不是小数就行,例如10,200,56,115,222等;最后一个参数lpTimerFunc是指向一个回调函数的指针,这个与WindowProc类似的,但是这个参数是可以为NULL的。
当该参数为NULL时,在WindowProc中你就要捕捉WM_TIMER消息,不过,这个是消息是低优先级的,系统会在处理完其他消息后,闲着没事干才会来处理WM_TIMER消息。如果lpTimerFunc参数不为NULL,就不用捕捉WM_TIMER了,直接在回调函数中处理即可。
如果所使用的Timer的ID已经存在,那么就会以新的Timer来取代原有的Timer。
KillTimer好说,就是销毁计时器,其中,Timer的ID要与前面SetTimer时用的ID保持一致,这个就不用特别说明了,你拿着你的借书证去图书馆借书,到还书的时候,你当然不会拿别人的借书证去还书吧?
理论的东西都是说多无益,还是用实例来说话吧。
先简单说说这个例子,主要运用计时器,每隔一秒(1000毫秒)执行一次,但每次的情况不同,所以用一个BOOL类型的变量来标识,如果为TRUE就在WM_PAINT事件中把窗口的客户区域填充为红色,如果为FALSE就不填充。如此,就可以使得窗口呈现出一闪一闪的效果。
我只贴出核心代码,完整的例子我随后上传到【资源】中。
~~~
// Timer的回调函数
VOID CALLBACK TimerProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ UINT_PTR idEvent,
_In_ DWORD dwTime
)
{
isBorderDrawed = !isBorderDrawed;
RECT rect;
GetClientRect(hwnd,&rect);
InvalidateRect(hwnd, &rect, TRUE);
}
~~~
~~~
/* 处理WM_PAINT消息 */
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: 在此添加任意绘图代码...
// 获取窗口边框矩形
RECT rect;
GetClientRect(hWnd, &rect);
if (isBorderDrawed)
{
HBRUSH hb = CreateSolidBrush(RGB(255,0,0));
FillRect(hdc,&rect, hb);
SelectObject(hdc,hb);
}
EndPaint(hWnd, &ps);
break;
~~~
结果就如下面两图所示。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd313f1d61.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd3140f21e.jpg)
';
(23):渐变颜色填充
最后更新于:2022-04-01 20:07:56
GradientFill函数可以对特定的矩形区域或者三角形区域进行渐变颜色的填充。我们先来看看GradientFill函数到底长得什么样子,帅不帅。
~~~
BOOL GradientFill(
_In_ HDC hdc,
_In_ PTRIVERTEX pVertex,
_In_ ULONG nVertex,
_In_ PVOID pMesh,
_In_ ULONG nMesh,
_In_ ULONG ulMode
);
~~~
参数涉及结构体,于是有些人就开始感到痛苦了,调用函数一遇到结构体就会痛苦,而这个函数更痛苦,还要结构体的数组。
人家达摩祖师说了,痛苦是你的事,和API无关,它又不会因为你痛苦就变得简单易用,该简单的就简单,该复杂的还是那么复杂。我们来分析一个吧。首先第一个hdc就不用我说了,就是某个上下文,这个上下文你就理解为和谁有关,就是谁的句柄,句柄是一种资源标识,以前说过了,就好比身份证号码可以唯一标识你的身份一般(假身份证除外)。所以,这个hdc就是决定你要把渐变颜色画到哪里,画到窗口上就是窗口的HDC,画到桌面上就是桌面的HDC。
第二个参数,注意,是TRIVERTEX结构体的数组,数组中每一个TRIVERTEX结构体的变量指定一个渐变点的坐标和颜色,颜色是ARGB四通道的,也就是说你可以玩透明效果,A,R,G,B每个值的范围,MSDN上有说明是The color information of each channel is specified as a value from 0x0000 to 0xff00,这个范围用十进制表示到底是多少,自己打开“计算器”算一下就知道了,Win7以后的计算器程序严重强大,用来算命都可以,更别说进制换算了。
如果你写C++程序多了,你就会猜到第三个参数是什么了,凡是涉及到传数组作为参数的,后面肯定要带一个参数指明数组中有多少个元素。因为C语言的数组与托管语言不同,其实它是善变的,你定义了int a[3] ....,然后你赋值的时候,赋到a[5]都可以,虽然会报错,但是你不妨试试,即使报错,但是仍然可以取到值,因为内存分布是连续的。
不用说,nVertex 就是指明前面pVertex的个数;后面一个pMesh又是一个结构体的数组,但它是“雄雌同体”的,可以是GRADIENT_TRIANGLE结构体的数组,也可以是GRADIENT_RECT结构体的数组,具体得看你想怎么填充,从名字就知道,GRADIENT_TRIANGLE是三角形填充,就是填充出来的区域是三角形的,这个应该好理解,就是你的内裤的形状;而GRADIENT_RECT更好理解了,矩形,就是小学老师教你的长方形,正方形。
接着这个nMesh 参数你肯定知道了,不解释了,你懂的。
最后一个参数就是指定怎么填充,无非就是水平填过去,还是垂直填下来之类的。
那么,TRIVERTEX数组与GRADIENT_RECT或GRADIENT_TRIANGLE数组是如何对应的呢?
GRADIENT_TRIANGLE是定义三角形的,要确定一个三角形就得要三个点,所以这些点就从TRIVERTEX数组来,每个TRIVERTEX变量定义一个点。
比如,TRIVERTEX数组有六个元素,就有六个点,刚好可以定义两个三角形,GRADIENT_TRIANGLE中的三个成员就是三角形三个点的索引,这些索引就是TRIVERTEX数组中的元素的索引,六个点,索引是0,1,2,3,4,5,接着,如果第一个GRADIENT_TRIANGLE中的三个成员分别设为0,1,2,则六个点中前三个点就定义了第一个三角形,如果第二个GRADIENT_TRIANGLE的三个成员设置为3,4,5,那么,六个点中的后三个点组成一个三角形。如果是1,4,5,就说明六个点中的第二个,第五个和第六个点将组成一个三角形。
GRADIENT_RECT就更好理解了,两个点就可以确定一个矩形,即左上角的点,右下角的点,如果渐变点有四个,正好可以组成两个矩形。0和1,2和3.
下面的代码将画出两个矩形的填充区域。
~~~
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: 在此添加任意绘图代码...
TRIVERTEX trives[4];
trives[0].x=10;
trives[0].y=12;
trives[0].Alpha=0xff00;
trives[0].Red=0xe100;
trives[0].Green=0x00cc;
trives[0].Blue=0xa3c3;
trives[1].x=188;
trives[1].y=80;
trives[1].Alpha=0xff00;
trives[1].Red=0x00fa;
trives[1].Green=0x0068;
trives[1].Blue=46;
trives[2].x=280;
trives[2].y=190;
trives[2].Alpha=0xff00;
trives[2].Red=0x0004;
trives[2].Green=0x0017;
trives[2].Blue=0xff00;
trives[3].x=500;
trives[3].y=320;
trives[3].Alpha=0xff00;
trives[3].Red=0xfac0;
trives[3].Green=0xcccc;
trives[3].Blue=0xcef0;
// 定义渐变区为矩形,并确定其点
GRADIENT_RECT rects[2];
rects[0].UpperLeft=0;
rects[0].LowerRight=1;
rects[1].UpperLeft =2;
rects[1].LowerRight=3;
GradientFill(hdc,trives,4,rects,2,GRADIENT_FILL_RECT_H);
EndPaint(hWnd, &ps);
break;
~~~
最后就得到如下图所示的效果。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd313c3713.PNG)
好,下面我们看看三角形的。
~~~
// 三角形
GRADIENT_TRIANGLE triangles[2];
triangles[0].Vertex1=2;
triangles[0].Vertex2=0;
triangles[0].Vertex3=3;
triangles[1].Vertex1=3;
triangles[1].Vertex2=1;
triangles[1].Vertex3=0;
// 矩形填充
GradientFill(hdc,trives,4,rects,2,GRADIENT_FILL_RECT_V);
// 三角形填充
GradientFill(hdc,trives,4,triangles,2,GRADIENT_FILL_TRIANGLE);
~~~
我们用的还是前面填充矩形的四个点,那有人会问了,不对啊,四个怎么能弄出两个三角形呢,不是应该要六个点吗?是啊,是六个点,但是这个四个点是可以重复利用,现在都说要环保,所以循环利用,2-0-3组成一个三角形,3-1-0又组成一个三角形。反正就是一个排列组合,你只要每个三角形弄足三个点就行了。前面的矩形同样道理,你只要每个矩形弄够两个点就行了。这就很像WPF中使用的三角形建模的三维图形同理,点的集合我就定义这么多个,但是我在其中任取三个就可以组成一个“模型单元”。
与前面的代码合并执行,最后得到这种效果。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd313d8cf7.PNG)
';
(22):抓取屏幕
最后更新于:2022-04-01 20:07:54
关于如何拷贝屏幕并保存,这里已经有现成的例子,我也不必去Copy人家了,我一向不喜欢Copy。这里有一个完整的例子,可以看看。
[http://msdn.microsoft.com/EN-US/library/windows/desktop/dd183402(v=vs.85).aspx](http://msdn.microsoft.com/EN-US/library/windows/desktop/dd183402(v=vs.85).aspx)
把屏幕的内容复制到窗口的客户区域中,通常会用BitBlt函数,函数的功能是把一块颜色数据从一个DC复制到另一个DC,这个我也不知道怎么翻译才能通俗一点。这样说吧,就是从源设备上下文的图形表面截取一个矩形区域并且复制到另一个设备上下文的区域中。就像我们要做一个截屏工具一样,把屏幕的一部分内容复制到窗口上。
下面呢,我用另一个函数来进行拷贝——StretchBlt函数,这个函数与BitBlt差不多,不过,它有一点,就是可以根据目标的区域对源图像进行拉伸。
注意看代码。
~~~
{
// 屏幕的DC
HDC hdcScreen = GetDC(NULL);
// 本窗口的DC
HDC hdcWindow = GetDC(hWnd);
// 屏幕的宽度
int scrWidth = GetSystemMetrics(SM_CXSCREEN);
// 屏幕的高度
int scrHeight = GetSystemMetrics(SM_CYSCREEN);
// 窗口的客户区域
RECT rectClient;
GetClientRect(hWnd, &rectClient);
// 使用StretchBlt进行复制
StretchBlt(hdcWindow,0,0,rectClient.right,rectClient.bottom,hdcScreen,0,0,scrWidth,scrHeight,SRCCOPY);
// 释放DC
ReleaseDC(NULL, hdcScreen);
ReleaseDC(hWnd, hdcWindow);
}
~~~
现在,我们要明确,我们是要把屏幕上的东西往窗口区域复制,所以我们想到,必须先有两个DC,一个是屏幕的DC,另一个就是窗口的DC。DC通过GetDC函数可以获取,将参数设置为NULL,也就是获得主屏幕的DC句柄了,NULL可以认为获取桌面的DC。
获取到DC后,我们还必须知道源区域的宽度和高度,以及目标窗口区域的宽度和高度。
源区域是屏幕,所以我们只要知道了当前屏幕的高度和宽度就可以了,GetSystemMetrics(SM_CXSCREEN)返回当前屏幕的宽度,GetSystemMetrics(SM_CYSCREEN)获得当前屏幕的高度。
而窗口的区域大小呢?我们不妨先得到窗口客户区域的矩形大小,用GetClientRect函数填充一个RECT结构体,这个结构体的right成员就是窗口客户区域的宽度,bottom成员就是窗口客户区域的高度了。
好了,有了这些基本参数,后面的事情就好办了。
BOOL StretchBlt( _In_ HDC hdcDest, _In_ int nXOriginDest, _In_ int nYOriginDest, _In_ int nWidthDest, _In_ int nHeightDest, _In_ HDC hdcSrc, _In_ int nXOriginSrc, _In_ int nYOriginSrc, _In_ int nWidthSrc, _In_ int nHeightSrc, _In_ DWORD dwRop);
以上是StretchBlt函数的声明,带“Dest”字样的都与目标区域有关,带“Src”字样的都与源区域有关,至于什么含义,看参数名字就知道了,你懂的。
最后一个参数,是一个标志,就是告诉函数用什么形式去复制,我们这里使用SRCCOPY就是按源来的数据复制,不作修改。这个参数可以在Wingdi.h找到,说明在MSDN文档。
现在,你可以看看它的效果。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd3132a26e.PNG)
再如,如果我把StretchBlt的最后一个参数改为NOTSRCCOPY,这就使得源区域与当前区域进行取反,也就是我们常说的“反色”。再看看。
StretchBlt(hdcWindow,0,0,rectClient.right,rectClient.bottom,hdcScreen,0,0,scrWidth,scrHeight,NOTSRCCOPY);
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd31359d88.PNG)
但是你会发现,当进行多次操作,窗口上显示的有一部分变成了正色,为什么呢?因为在你第二次截屏时,窗口中显示了上一次的截屏的内容,是反色的,而对反色再进行反色,不就变成了正色了吗?负负得正,所以才会看到像下图所示的效果。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd31388946.PNG)
';
(21):复制&粘贴&剪贴板操作
最后更新于:2022-04-01 20:07:51
我要提醒一下大家,看了我的博文学到的知识,千万不要用于实际开发,不然你会被你的上司骂:“妈的,这些东西哪来的,从来没有人这样做过。”不信你试试,脑细胞被冻结的经理或者技术总监们肯定会这样说的。
如果是一些有X年工作经验(X永远不可能大于100)的程序员肯定会说:“你怎么这么不成熟?”你如果被别人这么说之后,不知道你会不会很伤心,或者很生气?
我呢,曾经被N个人这样教育过,不过你猜猜我当时的心情,我非常高兴,喜悦。为什么呢?
你不妨想想,当一个人说你不成熟的时候,你说他其实在说什么,他其实是在说他自己很成熟,就因为他自己熟得快腐烂了,满身恶臭,才会显得你不成熟,仔细想想,是不是这个事儿?
那么,成熟到底好不好呢?我相信小学生都有这样的常识,当一个果子熟透了的时候,会怎么样?它会从树枝上高空坠落,然后狠巴巴地摔到地上,粉身碎骨。呵呵,所以,你现在明白了吧,当别人说我不成熟的时候,我会非常高兴,我心里想:“快了快了,你快完蛋了。”
----------------------------------------------------------------------------------------------------------------
好,牛皮吹完,乐一乐。下面开始干正事,今天咱们来认识一下怎么操作粘贴板,即复制和粘贴数据。我不知道大家看不看恐怖片,反正我现在不得不吓你一回。这个粘贴板的操作,其实挺痛苦的,所以,如果在实际开发中,我肯定用CLR的类来弄,是的,这是我的做事原则,哪种方法最简单就用哪种,这叫什么?效率最大化,只有闲着没事干的人才会简单问题复杂化。
不过呢,毕竟这里咱们要了解一下在Win32下操作粘贴板的,所以呢,我还是用API来解决,至于CLR方法,相信你比我更会用,你不信算了,反正我信了,我不是菜鸟,但我是菜鹤。
读写粘贴板就像我们上厕所一样,首先打开厕所门(调用OpenClipboard函数),然后大动作(SetClipboardData或GetClipboardData),干完了出来,关上厕所门(调用CloseClipboard)。
我说它有点痛苦是因为操作的时睺,与我们平常读写一些数据不同,数据写入到剪贴板后就由操作系统接管了,期间你不能任意读写,就像公共厕所是提供给你用的,你不能在里面给人家装修,拆掉人家的东西。
### 一、复制数据
我这里就不弄太复杂了,就复制一串文本吧,这也常用。先看看代码。
~~~
//复制内容
//打开剪贴板
OpenClipboard(hdlg);
//清空剪贴板
EmptyClipboard();
//向剪贴板中放东西
HWND hedt = GetDlgItem(hdlg, IDC_EDTCPY);
WCHAR ntext[100];
SendMessage(hedt, WM_GETTEXT, (WPARAM)100, (LPARAM)ntext);
//分配内存
HGLOBAL hgl = GlobalAlloc(GMEM_MOVEABLE, 100 * sizeof(WCHAR));
LPWSTR lpstrcpy = (LPWSTR)GlobalLock(hgl);
memcpy(lpstrcpy, ntext, 100 * sizeof(WCHAR));
GlobalUnlock(hgl);
SetClipboardData(CF_TEXT, lpstrcpy);
//关闭剪贴板
CloseClipboard();
~~~
大家看到,在打开剪贴板后,要先清空一下,调用EmptyClipboard函数,把里面的东西清了,才能放东西进去。但是我们不能直接用SetClipboardData设置数据,不然你试试,会失败,因为数据虽然是我们放进去的,但他归系统管,虽然我们的房子是自己买的,但也归小区物业公司管理。
因此,我们要通过内存拷贝来完成,GlobalAlloc函数分配全局内存块,不过标志参数要用GMEM_MOVEABLE,为什么,看MSDN怎么说的。
你可能会问,SetClipboardData中的参数是HANDLE类型的,为什么不用转换就能与
If SetClipboardData succeeds, the system owns the object identified by the hMem parameter. The application may not write to or free the data once ownership has been transferred to the system, but it can lock and read from the data until the CloseClipboard function is called. (The memory must be unlocked before the Clipboard is closed.) If the hMem parameter identifies a memory object, the object must have been allocated using the function with the GMEM_MOVEABLE flag.
这段话不知道你看懂了没有,反正我看不懂,很难翻译,干脆不译了。就是这段话告诉了我们,用GlobalAlloc分配内存时要用GMEM_MOVEABLE标志。既然内存是动的(当然物理内存是固定的),那我们在拷贝前当然要把它锁定,拷贝完了再解锁,防止这块内存被外星人意外修改了。
### 二、粘贴数据
复制完了,就粘贴了。
~~~
else if(LOWORD(wParam) == IDC_BTNPAST)
{
HWND hedtPas = GetDlgItem(hdlg, IDC_EDTPAST);
OpenClipboard(hdlg);
//判熂是否为文本内容
if(IsClipboardFormatAvailable(CF_TEXT))
{
//取出数据
HGLOBAL hg = GetClipboardData(CF_TEXT);
//锁定内存块
LPWSTR wstr = (LPWSTR)GlobalLock(hg);
if(wstr != NULL)
{
SendMessage(hedtPas, WM_SETTEXT, NULL, (LPARAM)wstr);
}
GlobalUnlock(hg);
}
CloseClipboard();
}
~~~
这里干吗要用IsClipboardFormatAvailable来检测一下剪贴板中是不是CF_TEXT格式的数据呢?因为在你复制了文本后,有可能在这期间其他程序把其他数据放到剪贴板上了,所以,要检查一下好,谨慎一点总是没有错的。
也许你会问,GetClipboardData不是返回HANDLE类型吗?怎么可以赋给HGLOBAL类型的变量而不用转换呢,你把开对应头文件看看就懂了,HGLOBAL就是HANDLE。
好了,就这样吧,只要你简单了解操作过程就够了,不必深究,呵呵,因为有比这更简单的方法。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd3131081a.PNG)
';
(20):浏览文件夹
最后更新于:2022-04-01 20:07:49
最近忙于一些相当无聊的事情,还没忙完,不过,博客还是要写的,不然我头顶上会多了几块砖头。
在上一篇博文中,我们浏览了文件,今天我们也浏览一下目录,如何?
浏览目录我们同样有两个规矩,用托管类库的我就不说了,相信大家两分钟就能弄好。重点还是说说第二个规矩,用API方式。我们用到的是Shell函数,一提到Shell,可能有朋友会感到痛苦,因为它很多函数涉及到COM,不过,也没什么了,反正我的哲学理念是立足于实战。所以,我给大家总结了思路。
就拿今天我们要用到的SHBrowseForFolder函数来说吧,先说说这个函数的用途吧,一句话概括:*显示一个对话框,让你可以选择一个目录*。
COM对象需要先初始化,再用,用完了释放,根据这个思路,大的步骤如下:
1、CoInitialize(NULL); //初始化。
2、调用函数。
3、CoUninitialize(); //释放。
这个动作就好比你去租用商家的自行车去游玩,别人只是把资源(自行车)借给你用,你给押金(引用计数),等到你耍完了,再去把资源还给人家。
要调用SHBrowseForFolder函数,先初始化它的参数,也就是BROWSEINFO结构体,这个家伙初始化后,将地址传给SHBrowseForFolder的lpbi参数。BROWSEINFO结构你得设置以下成员。
~~~
PIDLIST_ABSOLUTE pIDLIST;//函数返回的值存在这里
BROWSEINFO brsInfo;
WCHAR fdpath[MAX_PATH]; //接收选择目录的字符数组
。。。。。。。。。。。。。。。。。。。。
brsInfo.hwndOwner = hWnd; //拥有者窗口
brsInfo.pidlRoot = NULL; //根目录为桌面
brsInfo.pszDisplayName = fdpath; //返回的被选择的路径
brsInfo.lpszTitle = L"请选择目录"; //对话框标题
brsInfo.ulFlags= BIF_RETURNONLYFSDIRS | BIF_DONTGOBELOWDOMAIN | BIF_NEWDIALOGSTYLE; //不显示网络路径
brsInfo.lpfn = NULL; //没有回调
brsInfo.lParam = NULL; //没有额外参数
~~~
之后就可以调用函数了。
~~~
// 初始化COM组件,用完后记得释放
CoInitialize(NULL);
pIDLIST = SHBrowseForFolder(&brsInfo);
if(pIDLIST)
{
//取出对话框中用来显示结果的控件
HWND hStatic = GetDlgItem(hfDlg, IDC_DISPLAY);
SendMessage(hStatic,WM_SETTEXT, NULL, (LPARAM)fdpath);
}
CoUninitialize();
// 释放IDLIST
CoTaskMemFree((LPVOID)pIDLIST);
~~~
由于PIDLIST也是COM对象资源,通过调用CoTaskMemFree函数将其释放。
现在,运行一下应用程序。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd312bd2da.PNG)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd312d8710.PNG)
聪明的你会发现,虽然我们得到了选择的目录名字,但不是完整路径,通常我们是需要完整的路径的。这个也不难,我们再请出另一个函数——SHGetPathFromIDList,它可以帮我们得出完整的路径。
所以,我们把上面的代码修改一下。
~~~
if(pIDLIST)
{
//得到完整路径
SHGetPathFromIDList(pIDLIST, fullPath);
//取出对话框中用来显示结果的控件
HWND hStatic = GetDlgItem(hfDlg, IDC_DISPLAY);
SendMessage(hStatic, WM_SETTEXT, NULL, (LPARAM)fullPath);
//SendMessage(hStatic,WM_SETTEXT, NULL, (LPARAM)fdpath);
}
~~~
现在再看看结果,这回你肯定满意了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd312edac8.PNG)
-------------- 【C++ Code】 -------------------------
~~~
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
PIDLIST_ABSOLUTE pIDLIST;//函数返回的值存在这里
BROWSEINFO brsInfo;
WCHAR fdpath[MAX_PATH]; //接收选择目录的字符数组
WCHAR fullPath[MAX_PATH]; //完整路径
static HWND hfDlg;
switch (message)
{
case WM_CREATE:
hfDlg = CreateDialog(hInst, MAKEINTRESOURCE(IDD_FORMVIEW), hWnd, NULL);
ShowWindow(hfDlg,SW_SHOW);
break;
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
case IDM_BROWSFOLDER:
brsInfo.hwndOwner = hWnd; //拥有者窗口
brsInfo.pidlRoot = NULL; //根目录为桌面
brsInfo.pszDisplayName = fdpath; //返回的被选择的路径
brsInfo.lpszTitle = L"请选择目录"; //对话框标题
brsInfo.ulFlags= BIF_RETURNONLYFSDIRS | BIF_DONTGOBELOWDOMAIN | BIF_NEWDIALOGSTYLE; //不显示网络路径
brsInfo.lpfn = NULL; //没有回调
brsInfo.lParam = NULL; //没有额外参数
// 初始化COM组件,用完后记得释放
CoInitialize(NULL);
pIDLIST = SHBrowseForFolder(&brsInfo);
if(pIDLIST)
{
//得到完整路径
SHGetPathFromIDList(pIDLIST, fullPath);
//取出对话框中用来显示结果的控件
HWND hStatic = GetDlgItem(hfDlg, IDC_DISPLAY);
SendMessage(hStatic, WM_SETTEXT, NULL, (LPARAM)fullPath);
//SendMessage(hStatic,WM_SETTEXT, NULL, (LPARAM)fdpath);
}
CoUninitialize();
// 释放IDLIST
CoTaskMemFree((LPVOID)pIDLIST);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: 在此添加任意绘图代码...
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
~~~
';
(19):浏览和打开文件
最后更新于:2022-04-01 20:07:47
在应用程序中,我们很经常要实现的功能,是Open文件或保存文件对话框,让用户来选择一个或N个文件。本文我将介绍两种思路,第一种方法较为复杂,第二种方法较为简单。
### 方法一:老规矩
这是一种传统方法,使用GetOpenFileName或者GetSaveFileName函数,看名字就知道,前者用来打开文件,后者是保存文件,这两个函数的用法是一样的,因此,我只演示其中一个。
无论你使用哪个函数,都要涉及一个结构体——OPENFILENAME。关于它的成员,我就不一一来说了,挑几个有用的扯一扯。
lStructSize:结构的大小,弄个sizeof就行了。
lpstrFilter:设置过滤器。注意啊,这个过滤器字符串和.NET中的写法不同,.NET的写法是从VB6继承过来,可以写成“垃圾文件 | *.rbs | 老鼠文件 | *.mos”,我们这里不是用“|”来分隔的,而是用“\0”分隔,而**结尾是两个NULL,即两个“\0”**。
nFilterIndex:过滤器索引,设置了N个滤过的后缀,设置默认选择哪个,第一个为1,第二个为2,第三个为3,依此类推,是从1开始的,不是0。
lpstrFile:文件名,包含完整路径,当对话框关闭后,我们就是从这个成员把用户选择的文件名取出。
nMaxFile:文件名的长度,一定要足够大,不然就装不下了,对路径长度,系统是有限定的,用MAX_PATH宏就可以了,WCHAR myFilename[MAX_PATH]。
Flags:标志位。主要决定对话框应具备哪些特征和行为,如是否检查目标文件已存在。
哦,这个可能看了没感觉,看看例子代码,一目了然。
~~~
OPENFILENAME opfn;
WCHAR strFilename[MAX_PATH];//存放文件名
//初始化
ZeroMemory(&opfn, sizeof(OPENFILENAME));
opfn.lStructSize = sizeof(OPENFILENAME);//结构体大小
//设置过滤
opfn.lpstrFilter = L"所有文件\0*.*\0文本文件\0*.txt\0MP3文件\0*.mp3\0";
//默认过滤器索引设为1
opfn.nFilterIndex = 1;
//文件名的字段必须先把第一个字符设为 \0
opfn.lpstrFile = strFilename;
opfn.lpstrFile[0] = '\0';
opfn.nMaxFile = sizeof(strFilename);
//设置标志位,检查目录或文件是否存在
opfn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
//opfn.lpstrInitialDir = NULL;
// 显示对话框让用户选择文件
if(GetOpenFileName(&opfn))
{
//在文本框中显示文件路径
HWND hEdt = GetDlgItem(hDlg, IDC_EDTFILENAME);
SendMessage(hEdt, WM_SETTEXT, NULL, (LPARAM)strFilename);
}
~~~
ZeroMemory函数前面说过了,你就当作它用来初始化结构就行了。
你可能会疑问,不是说lpstrFilter的字符串是两个NULL结尾的吗,为什么代码中只有一个?因为你输入的字符串会在后面自动加了个‘\0’,所以我们加一个就OK,后面自动加上一个,就两个了。
下图是执行结果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd3116f8b1.PNG)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd3118f933.PNG)
GetSaveFileName的用法也一样,有兴趣的朋友可以回家自己玩一下。
### 方法二:新规矩
这个新规矩是谁定的?哈哈,我定的。
都说知识是可以综合运用的,所以,下面的方法是灵活运用知识了。为啥?记得吧,.NET类库其实也为我们封装了,大家玩过Windows Forms开发肯定知道,在System.Windows.Forms下面的。是啊,既然有了现成的,我们干吗不拿来用呢。
首先,打开项目属性,按下图的方法,让项目支持 /clr ,然后点击“应用”。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd311a1708.PNG)
引用需要用到的程序集。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd311b96c2.PNG)
在代码文件中引入命名空间。
~~~
using namespace System;
using namespace System::Windows::Forms;
~~~
然后实现打开文件的功能。
~~~
if(LOWORD(wParam) == IDC_BTNOPWTCLR)
{
OpenFileDialog ^dlg = gcnew OpenFileDialog;
dlg ->Filter = L"所有文件|*.*|图片文件|*.jpg";
dlg->FilterIndex = 1;
dlg->CheckFileExists = true;
if (dlg ->ShowDialog() == DialogResult::OK)
{
//取得文件名
}
}
~~~
但是,当获取文件名的时候,遇到了一个严重问题了,在CLR中,我们取到的文件名是System::String ^类型的,可是我们这里的Win32程序需要wchar_t类型的字符串,这怎么办?能进行转换吗?
微软早就想到这个问题,所以,它给我们写了一个vcclr.h头文件,里面有一个PtrToStringChars函数,使用它就可以将托管的字符串转为标准字符串了。
~~~
if (dlg ->ShowDialog() == DialogResult::OK)
{
//取得文件名
pin_ptr strFileName = PtrToStringChars(dlg->FileName);
HWND hEdt = GetDlgItem(hDlg, IDC_EDTFILENAME);
SendMessage(hEdt, WM_SETTEXT, NULL, (LPARAM)strFileName);
}
~~~
但是,问题仍未解决,运行之后你会很惊喜地收到一个异常信息。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd311cc054.PNG)
运气真的太好了,那么这个怎么解决呢?再次打开项目属性窗口,找到“链接器”-“高级”,把CLR线程模型改为STA就可以了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd311dfe4e.PNG)
现在运行应用程序,估计没有问题了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd31278e4b.PNG)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd31297003.PNG)
代码我随后上传。
好了,好了,现在大功告成了。拜拜。
';
(18):使用对话框的两个技巧
最后更新于:2022-04-01 20:07:45
相信大家知道对话框怎么用了,就是先用“资源编辑器”设计一个对话框,然后在代码中加载处理。今天,我向大家分享两个使用对话框的技巧,还是比较实用的。不用担心,先喝杯茶,很简单的,一点也不复杂,总之,看俺写的博客,你不会有思想压力的。放心好了。
这两个技巧分别是:
### 1、在显示某个对话框时,向对话框传递数据。
这个我相信大家能够理解,就好比咱们在做Web开发的时候,在打开一个页面时,可能会考虑向目标页面传一些参数,如这样http://www.牛B.com/yyy.aspx?tid=19999,是吧,我们都很经常这样做,所以,在玩Win32的时候,我们也同样希望这样。
我们知道,要显示一个模态对话框,可以调用DialogBox函数,这个不难,我有理由相信大家只需稍稍研究一下,一定会玩的,就像遥控飞机那么简单。如果你认真阅读MSDN,你一定会发现有这么一个函数:DialogBoxParam。怎么样,是不是和前面的那个DialogBox很像,只是脑袋后面多了几根汗毛罢了,而且,从Param就可以猜到,应该与参数传递有关。对,猜得相当好,我们在显示某个对话框时,要向对话框传参数,就是用它了。
顺便提一下,什么是模态对话框呢?模态对话框就是说如果这个对话框弹出来(显示)了,你就不能操作父窗口或者其上面的控年了,程序也会在弹出对话框后停下,直到操作完成了,你把对话框关了,程序才会继续,你才能操作其他控件,就好像你开车上班,行驶到某个路口,前面有几辆车突然停了下来,车上下来一群不男不女的人,在马路上打起架来了,导致交通严重受阻,你的车夹在车流中间,既不前进,也不能后退,就卡在那里,你估计只好呆在车里,拿出MP5,看看恐怖片,等到交通恢复正常了,才能继续行车。
看看这个DialogBoxParam函数的定义:
~~~
INT_PTR WINAPI DialogBoxParam(
_In_opt_ HINSTANCE hInstance,
_In_ LPCTSTR lpTemplateName,
_In_opt_ HWND hWndParent,
_In_opt_ DLGPROC lpDialogFunc,
_In_ LPARAM dwInitParam
);
~~~
前面几个参数估计不用我介绍了,大家都认识,比周杰伦和刀郎还要出名。重点是最后一个参数,它是一个LPARAM类型的,我记得Win32的许多类型的命名是有规律的,你看看,凡是P开头或者LP开头都是指针类型,不信你可以总结一下。
所以这个参数肯定是指定某种未知生物的指针,指针存的是地址,所以我们通过把我们要传给对话框的数据的地址传给这个参数,这样,对话框的消息处理函数如果想取到你传给它的数据,就从这个地址去找。就好像你同学介绍了一位美女给你认识,他告诉你这位MM的QQ号,然后你想泡她你就知道怎么做了,加QQ聊呗。
那么,这个参数会传到哪里去呢,怎么取出来呢?这个参数传给了WM_INITDIALOG消息的lParam参数,所以,在对话框的消息处理函数中,捕捉WM_INITDIALOG消息,从它的lParam参数就可以找到数据的首地址了。
显示对话框并传递参数。
~~~
DialogBoxParam(glo_hInst,
MAKEINTRESOURCE(IDD_DLG1),
hwnd,
(DLGPROC)DialogProcWithPrm,
(LPARAM)L"Win32程序好玩不?");
~~~
我传递了一些文本给它,即“Win32程序好玩不?”,接着在响应WM_INITDIALOG消息时就接收并显示这个参数。
~~~
switch (uMsg)
{
case WM_INITDIALOG:
{
LPCWSTR param = (LPCWSTR)lParam;
//取得显示文本控件的句柄
HWND stDisplay = GetDlgItem(hwndDlg, IDC_STINFO);
//设置文本
SendMessage(stDisplay, WM_SETTEXT, NULL, (WPARAM)param);
}
return 0;
~~~
哈哈,不难吧。
### 2、获取对话框返回的数据。
前面是传数据给对话框,现在要反过来,是获得从对话框传回来的数据。刚才我们说了,模态对话框显示时,父窗口会在等待,等啥,就是等DialogBox返回,它返回的类型是INT_PTR,所以,如果要返回比较复杂的数据,可以用它来保存指针,因为指针本质上就是一个数值。
~~~
// 显示对话框并取得返回的数据
LPCWSTR returnData = (LPCWSTR)DialogBox(glo_hInst,MAKEINTRESOURCE(IDD_DLG2),hwnd,(DLGPROC)DialogProcWithRtVl);
if(returnData != NULL)
{
//先将字符拷贝一次
WCHAR str[100];
wcscpy(str, returnData);
//显示从对话框返回的内容
MessageBox(hwnd, str, L"从对话框返回的数据", MB_OK);
}
break;
~~~
我这个例子是在对话框中输入一些字符,然后把这些字符串返回。所以,返回的内容是指向字符数组首地址的指针,故我直接把它转换为LPCWSTR类型的变量。
在对话框中,调用EndDialog结束对话框,并在第二个参数中传回数据。
~~~
if(LOWORD(wParam)== IDOK)
{
//获得Edit编辑框的句柄
HWND hEdit = GetDlgItem(hwndDlg, IDC_EDTIN);
//获得文本
WCHAR wText[100];
int len = Edit_GetText(hEdit, wText, 100);
EndDialog(hwndDlg, (INT_PTR)wText);//退出对话框并返回数据
}
~~~
例子运行之后的结果可以看看下面的截图。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd3112446c.PNG)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd31134eb5.PNG)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd3114b68f.PNG)
示例源代码随后我上传到资源区,有兴趣的朋友可以下载来玩一下。
';
(17):启动和结束进程
最后更新于:2022-04-01 20:07:42
这里我再次说明一下,我不知道为什么,现在的人那么喜欢走极端,估计是价值观都“升级”了的缘故吧。
我撰写这一系列Win32相关的文章,并不是叫大家一定要用Win32去开发项目,仅仅是给大家了解一下,Win32是个啥东西而已。
另外,在专访中,有些人也误认为我说C++非学不可,那也不是,我也提到过了,C++的学习可以使你懂得更多知识,为你以为学习各种技术打下基础。
今天,我们来了解一下,在应用程序中,如何启动一个进程,然后把这个进程结束掉。实现这一功能有替代方案,即使用.NET库中的System::Diagnostics::Process类来完成。在Win32中,启动一个进程可以理解为创建进程,所以可以调用CreateProcess函数,结束进程则调用TerminateProcess函数。
唯一标识一个进程的是PID,而我们要对进程进行各项操作,进程也存在于内存中,也可以视为一种资源,我们知识,操作图标资源有HICON,操作位图资源有HBITMAP,同样道理,对进程所作的操作也要通过一个标识符(句柄)——HANDLE。
CreateProcess函数的最后一个参数是一个指向PROCESS_INFORMATION结构体的指针,该结构的成员可以保存被创建进程的ID和句柄。
~~~
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess; //进程的句柄
HANDLE hThread; //主线程的句柄
DWORD dwProcessId; //进程ID
DWORD dwThreadId; //主线程ID
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
~~~
接下来我们要对已创建进程进行的一系列操作,都以hProcess为纽带,包括获取退出码和终止进程。
CreateProcess函数的最后两个参数都是指针,因此我们在使用时,要先定义STARTUPINFO和PROCESS_INFORMATION类型的变量,并使用ZeroMemory函数将其成员初始化。ZeroMemory函数可以将一段内存中的数据初始化为0.
示例代码如下:
~~~
PROCESS_INFORMATION pro_info; //进程信息
STARTUPINFO sti; //启动信息
//......
// 初始化两个结构体
ZeroMemory(&pro_info, sizeof(PROCESS_INFORMATION));
ZeroMemory(&sti, sizeof(STARTUPINFO));
~~~
接着调用CreateProcess创建进程.
~~~
CreateProcess(L"C:\\Windows\\System32\\calc.exe",NULL,NULL,NULL,FALSE,0,NULL,NULL,&sti,&pro_info);
~~~
调用成功后,PROCESS_INFORMATION结构体的hProcess就保存了被创建的进程的句柄了。
要结束进程,调用TerminateProcess函数,第一个参数就是刚才创建的时程的句柄,第二个参数是退出码,通过GetExitCodeProcess函数可以获取。
~~~
DWORD exitCode; //退出码
//.........
GetExitCodeProcess(pro_info.hProcess,&exitCode); //获取退出码
TerminateProcess(pro_info.hProcess, exitCode);
// 关闭句柄
CloseHandle(pro_info.hThread);
CloseHandle(pro_info.hProcess);
~~~
下面是这个示例的运行结果图,完整的代码我稍后上传到资源区中。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd310e5db2.PNG)
另外,再介绍一个在应用程序中运行其他程序的方法,那就是使用Shell函数——ShellExecute。
~~~
HINSTANCE ShellExecute(
_In_opt_ HWND hwnd,
_In_opt_ LPCTSTR lpOperation,
_In_ LPCTSTR lpFile,
_In_opt_ LPCTSTR lpParameters,
_In_opt_ LPCTSTR lpDirectory,
_In_ INT nShowCmd
);
~~~
我们这里只是运行一个程序,没有其他操作,所以,带 _In_opt_的可选参数可以直接NULL,lpFile指定我们要运行程序的路径,如果是一个exe文件,那lpParameters可以指定命令行参数,但我们这里不需要,所以继续NULL,最后一个参数好理解,和ShowWindow函数一样。
于是,用ShellExecute函数打开记事本程序,当然记得包含shellapi.h。
~~~
ShellExecute(NULL,NULL,L"C:\\Windows\\System32\\notepad.exe",NULL,NULL,SW_SHOWNORMAL);
~~~
看看结果,和前面效果差不多。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd31105b8b.PNG)
源代码稍后上传。
';
(16):ListView的多个视图
最后更新于:2022-04-01 20:07:40
在上一个例子中,我们只用到了ListView的Report视图,也就是详细视图。本文我们再把上一篇文章中所用的例子进行一下扩展,例子源码可以到俺的资源区下载。
我们为ListView中显示的数据加上图标,并且允许用户选择显示哪种视图,如大图标,小图标,详细信息等。
因为代码还比较长,我也不希望把代码全部放出来,在写完本文后,我会将源码上传到资源中。当然了,我也不可能说每一行代码都解释一遍,那也不现实,而且,这样也不好,我不能主观地去怀疑读者的领悟能力。
## 一、准备图标
既然要用到图标,为了简单方便,就用VS的资源编辑器随便画几下就有图标了,我们要准备两个图标,为什么呢?第一个图标是给EXE文件用的,而第二个图标是用在ListView中的。因为在生成的.exe文件的图标是选用我们最先添加到资源中的图标,为了使.EXE文件的图标和我们在ListView中用的图标不要一样(这样不好看),所以我们还是准备两个图标好一点。
图标中具备两个尺寸就够了——16*16和32*32,如果可能尽量用24位图,这样你能用更多的颜色。
## 二、如何切换视图
改变ListView的视图,可以使用ListView_SetView宏,发送LVM_SETVIEW消息也可以,不过使用宏更方便。它的第一个参数指定LV控件的句柄,第二个参数是设置用哪个视图。
LV_VIEW_DETAILS——详细视图。
LV_VIEW_ICON——大图标列表。
LV_VIEW_LIST——列表视图。
LV_VIEW_SMALLICON——小图标。
LV_VIEW_TILE——平铺,如果我没记错的话,这个视图是在XP时引入的。
## 三、ComboBox控件使用
为了可以让用户选择一个视图,自然要提供对应的操作界面,这是一种多选一的方式,用单选按钮和下拉拉表框都可以,不过,单选按钮要占用更多地方而且处理的消息更多,相对麻烦,所以,还是ComboBox好一些。
用ComboBox_AddString宏就可以向ComboBox中添加项,比如本例。
~~~
// 初始化ComboBox,以选择视图
hcbb = GetDlgItem(hDlg, IDC_CBVIEW);
ComboBox_AddString(hcbb, L"大图标");
ComboBox_AddString(hcbb, L"小图标");
ComboBox_AddString(hcbb, L"列表");
ComboBox_AddString(hcbb, L"详细");
~~~
当用户操作了ComboBox,它同样会发送一条WM_COMMAND消息,而我们之前已经响应过这条消息,看看例子,我们前面有一个“添加”按钮和一个“清除”按钮,它被点击后也会发送WM_COMMAND消息。因此,我们要做更详细的处理。
还记得吧,WM_COMMAND的wParam参数的低字节位表示发送该消息的控件的ID,高字节位表示“通知码”。lParam是控件的句柄。我们判断ID知道用户操作的是ComboBox控件还不够,因为我不知道用户对这个控件做了哪些操作,是弹出下拉列表?还是收起下拉列表?或者选择了另一个项?
而我们这里要做的是,看用户选择了哪个视图,我们的ListView控件就显示哪种视图,显然,在通知码中,我们是对CBN_SELCHANGE感兴趣,因为选择的索引值一旦改变,就会收到这个通知码。
~~~
case IDC_CBVIEW:
if (HIWORD(wParam) == CBN_SELCHANGE)
{
// 当前选择项的索引
int index = SendMessage((HWND)lParam, CB_GETCURSEL, 0, 0);
// 根据选择设置视图
DWORD lvView;
switch (index)
{
case 0:
lvView = LV_VIEW_ICON;
break;
case 1:
lvView = LV_VIEW_SMALLICON;
break;
case 2:
lvView = LV_VIEW_LIST;
break;
case 3:
lvView = LV_VIEW_DETAILS;
break;
default:
lvView = LV_VIEW_DETAILS;
break;
}
ListView_SetView(GetDlgItem(hDlg, IDC_LV), lvView);
}
break;
}
~~~
发送CB_GETCURSEL消息,可以得到ComboBox中当前选定项的索引值。
## 四、向ListView添加图标
先用ImageList_Create创建图像列表,然后用ImageList_AddIcon宏向列表中添加图标。因为我们要用大图标和小图标,所以要创建两个图像列表,一个放置大图标,另一种放置小图标,因为同一个Image List中放置的所有图像的尺寸必须相同。
~~~
// 初始化ImageList
hImgListSm = ImageList_Create(GetSystemMetrics(SM_CXSMICON),GetSystemMetrics(SM_CYSMICON),ILC_MASK,1,0);
hImgListLg = ImageList_Create(GetSystemMetrics(SM_CXICON),GetSystemMetrics(SM_CYICON),ILC_MASK,1,0);
hicon = LoadIcon(hgAppInst,MAKEINTRESOURCE(IDI_ITE));
// 添加图标
ImageList_AddIcon(hImgListSm, hicon);
ImageList_AddIcon(hImgListLg, hicon);
DestroyIcon(hicon);
~~~
接着,用ListView_SetImageList把Image List和ListView关联起来。
~~~
// 将ListView与ImageList关联
ListView_SetImageList(hListview, hImgListLg, LVSIL_NORMAL);
ListView_SetImageList(hListview, hImgListSm, LVSIL_SMALL);
~~~
在向ListView添加项时,设置LVITEM结构体的iImage字段为图像列表中对应图像的索引,因为我们只添加了一个图标,所以,索引是0.
~~~
LVITEM vitem;
vitem.mask = LVIF_TEXT | LVIF_IMAGE;
vitem.iImage = 0;
~~~
ImageList_Create返回的是一个句柄,它也是一种资源,所以,在不需要它了,就得记得把它销毁。在我们的对话框发生WM_DESTROY的同时将其销毁。
~~~
case WM_SYSCOMMAND:
if (wParam == SC_CLOSE)
{
// 销毁ImageList
ImageList_Destroy(hImgListLg);
ImageList_Destroy(hImgListSm);
DestroyWindow(hListview);//不再需要
DestroyWindow(hDlg);
}
return 0;
~~~
另外,补充一个小知识,要得到对话框中某个控件的句柄,可以调用GetDlgItem函数,这也是我们为什么要为控件设置ID值的原因。
程序运行后,就可以通过选择下拉列表来动态改变ListView的视图了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd3108719f.PNG)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd310a2de4.PNG)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd310b51f4.PNG)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd310cd47e.PNG)
好了,要过年了,这是新年前最后一篇博文,过完年后,我们继续。我也希望,后续能与大家一起分享更多的知识和编程技巧。
===================================
**祝大家**
**新春快乐,生活愉快,身体健康,工作顺利,明年更有成就。**
**祝各位以及各位的亲朋好友们,新年快乐。**
';
(15):ListView控件
最后更新于:2022-04-01 20:07:38
这个控件其实不用阿拉来介绍,因为它太常见了,就好像我们一出门就会看到妹子一样常见。当然也可以说,它是对ListBox的扩充。
在使用该控件之前,我先介绍VS的一个相当好玩的功能。
在代码文件的#include指令上右击,从弹出的菜单中选择“生成包含文件关系图”,如下图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d318950.png)
然后你喝一口咖啡,你会看到这样的东西:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d32fc5e.png)
这个关系图,演示了你的项目中的头文件,源文件以及外部引用文件之间的关系。把鼠标移到上面,滚动滑轮,可以缩放大小。把鼠标移到“外部”节点上,点击左边的向下箭头,可以看到本项目与外部头文件的关系。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d3522aa.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d362ddc.png)
所以,如果你的程序比较复杂,头文件众多,不妨试试这功能。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d3a0634.png)
=====================================================
下面我们来使用ListView来显示一组数据,我定义了一个结构体:
~~~
// 用于测试的结构体
struct STUDENTINFO
{
WCHAR Name[15];
WCHAR Age[3];
WCHAR Address[50];
};
~~~
假设它代表了一位学员的信息——姓名、年龄、地址。
我们要用ListView来显示一些学员的信息,显然,每一个学员信息都有三个字段,ListView有多种视图,如图:
列表小图标
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d3d2332.png)
大图标
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d3e4e8b.png)
平铺
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d40e187.png)
详细视图
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d423a8f.png)
我们要显示多个字段,故应选择最后一种视图。
好,下面我们就做一个练习,实例是检验学习成果的唯一标准。
1、新建一个对话框资源,在设计器中拖一个List Control和两个Button,List Control其实就是ListView控件。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d437e65.PNG)
设置View属性为Report。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd31047772.PNG)
2、在对话框消息处理函数中,处理WM_INITDIALOG消息,向ListView添加列。
~~~
case WM_INITDIALOG:
// 获取ListView控件的句柄
hListview = GetDlgItem(hDlg, IDC_LV);
// 设置ListView的列
LVCOLUMN vcl;
vcl.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_SUBITEM;
// 第一列
vcl.pszText = L"姓名";//列标题
vcl.cx = 90;//列宽
vcl.iSubItem = 0;//子项索引,第一列无子项
ListView_InsertColumn(hListview, 0, &vcl);
// 第二列
vcl.pszText = L"年龄";
vcl.cx = 90;
vcl.iSubItem = 1;//子项索引
ListView_InsertColumn(hListview, 1, &vcl);
// 第三列
vcl.pszText = L"地址";
vcl.cx = 200;
vcl.iSubItem = 2;
ListView_InsertColumn(hListview, 2, &vcl);
return 0;
~~~
向LV添加列,调用ListView_InsertColumn宏,注意它是宏不是函数(你也可以发送LVM_INSERTCOLUMN消息),其中有一个参数是指向LVCOLUMN结构体的指针,关于这个结构体的成员我就不说了,有兴趣的看MSDN。
这样,LV控件就有了三个列了,就像这样。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd3105bfa8.PNG)
3、另外两个按钮, 一个用来向LV中添加项,后一个是清除所有项。
还记得吧,要响应按钮单击,要处理WM_COMMAND消息,然后通过wParam参数的低字节位来判断用户点击了哪个按钮,指示了对应按钮的ID。
~~~
case WM_COMMAND:
if (LOWORD(wParam) == IDC_BTNADD)
{
STUDENTINFO stu[ ] = {
{ L"小刘", L"20", L"火星" },
{ L"老赵", L"21", L"木星" },
{ L"小胡", L"30", L"水星" },
{ L"老高", L"32", L"山沟一号" },
{ L"黄牛", L"24", L"不知哪个星球来的" },
{ L"王七", L"28", L"超人之乡" }
};
//求出数组中元素的个数
int arrCount = (int)(sizeof(stu) / sizeof(stu[0]));
LVITEM vitem;
vitem.mask = LVIF_TEXT;
for (int i = 0; i < arrCount; i++)
{
/*
策略:
先添加项再设置子项内容
*/
vitem.pszText = stu[i].Name;
vitem.iItem = i;
vitem.iSubItem = 0;
ListView_InsertItem(hListview, &vitem);
// 设置子项
vitem.iSubItem = 1;
vitem.pszText = stu[i].Age;
ListView_SetItem( hListview, &vitem);
vitem.iSubItem = 2;
vitem.pszText = stu[i].Address;
ListView_SetItem(hListview, &vitem);
}
}
else if(LOWORD(wParam) == IDC_BTNCLEAR)
{
// 清除ListView中的所有项
ListView_DeleteAllItems(hListview);
}
return 0;
~~~
首先,为了在LV中加入数据,声明了一个STUDENT数组,STUDENT结构体在前面定义的,表示一位学员的信息。由于这个数组在声明的时候,并没有指定元素个数,在后面执行for循环添加项之前,先要知道数组中有多少个元素。
方法是用sizeof运算符取出整个数组的字节长度,然后除以第一个元素的长度,这样就求出元素的个数了。
向LV添加项,调用ListView_InsertItem宏,注意添加方法,要先添加项,随后再用ListView_SetItem宏来设置子项的内容。由于两个宏使用相同的参数,所以在循环前,我们都用一个LVITEM,在循环中我们只改变它的项索引值和文本内容再传到ListView_InsertItem宏或ListView_SetItem宏,这样也免得多次分配内存数据。
清除LV中的所有项,直接用ListView_DeleteAllItems宏就可以了。
以上操作也可以通过发送对应消息来完成,不过,直接调用宏似乎比SendMessage方便。
最后,看一下最终结果。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd3106ddda.PNG)
由于这个例子相对有些复杂,稍后我把代码上传到[资源]中。
';
(14):用对话框作为主窗口
最后更新于:2022-04-01 20:07:35
前面我们在编写Win32应用程序的思路都是:
1、设计窗口类。2、注册窗口类。3、创建窗口。……
然而,当我们接触控件以后, 会发现一个问题,我们在窗口上放置控件实在不好弄,而资源中的对话框具有图形编辑器,操作起来相对方便。
于是,就有了一个想法:把对话框用作主窗口,那我们在布局控件时就轻松了。
其实这想法在网上已有牛人做了,不过,人家是用Win32汇编干的,当然,要把它变为C++就更简单了。
首先,介绍第一个法子,直接以桌面作为父窗口来创建对话框。
1、创建一个对话框资源,界面就发挥你的设计天赋了。
2、创建一个图标资源,既作为应用程序的图标(VS编译时会选择第一个添加到资源中的图标作为EXE的图标),也作为对话框的图标。
3、在代码中创建并显示对话框。
~~~
#include
#include "resource.h"
// 开启可视化效果
#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
INT_PTR CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
HINSTANCE hgInst;
int WINAPI WinMain(HINSTANCE hThisApp, HINSTANCE hPrevApp, LPSTR lpCmd, int nShow)
{
hgInst = hThisApp;
HWND hdlg = CreateDialog(hThisApp, MAKEINTRESOURCE(IDD_MY),GetDesktopWindow(),(DLGPROC)DlgProc);
if(!hdlg)
{
return 0;
}
ShowWindow(hdlg,SW_SHOW);
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
INT_PTR CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_INITDIALOG:
// 设置对话框的图标
SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)LoadIcon(hgInst, MAKEINTRESOURCE(IDI_APP)));
return 0;
case WM_SYSCOMMAND:
if(wParam == SC_CLOSE)
{
PostQuitMessage(0);//退出
}
return 0;
}
return (INT_PTR)FALSE;
}
~~~
这样,我们可以运行一下。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d29ff91.PNG)
=================================================================
第二种方法,我们还是遵照设计窗口类、注册窗口类、创建窗口等步骤,只是把对话框作为窗口的子级,对话框采用Form View视图。
在插入资源的对话框中,点击Dialog左边的图标,展开列表,在列表中选择IDD_FORMVIEW,然后点“新建”。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d2b1713.PNG)
然后你看到一个没有边框和标题栏的对话框。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d2cdeeb.PNG)
接下来我们可以放些控件上去。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d2e0634.PNG)
最后,在响应WM_CREATE消息时,创建并显示对话框,父级是我们的主窗口。
~~~
case WM_CREATE:
{
// 创建对话框
HWND hdlg = CreateDialog(hgAppInst, MAKEINTRESOURCE(IDD_MYFORM), hwnd, (DLGPROC)DlgProc);
// 显示对话框
ShowWindow(hdlg, SW_SHOWNA);
}
return 0;
~~~
最后运行结果如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d2f3590.PNG)
完整的代码清单:
~~~
#include
#include "resource.h"
#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
INT_PTR CALLBACK DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam);
HINSTANCE hgAppInst;
int WINAPI WinMain(HINSTANCE hThisApp, HINSTANCE hPrevApp, LPSTR lpCmd, int nShow)
{
// 设计窗口类
WNDCLASS wc = { };
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpfnWndProc = (WNDPROC)WindowProc;
wc.lpszClassName = L"supper_man";
wc.hInstance = hThisApp;
wc.style = CS_HREDRAW | CS_VREDRAW;
RegisterClass(&wc);
hgAppInst = hThisApp;
// 创建窗口
HWND hwnd = CreateWindow(L"supper_man", L"超级应用",
WS_OVERLAPPEDWINDOW, 40, 25, 380, 300, NULL, NULL, hThisApp, NULL);
if(!hwnd)
return 0;
ShowWindow(hwnd, nShow);
UpdateWindow(hwnd);
// 消息循环
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_CREATE:
{
// 创建对话框
HWND hdlg = CreateDialog(hgAppInst, MAKEINTRESOURCE(IDD_MYFORM), hwnd, (DLGPROC)DlgProc);
// 显示对话框
ShowWindow(hdlg, SW_SHOWNA);
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd,msg,wParam,lParam);
}
return 0;
}
// 处理对话框消息
INT_PTR CALLBACK DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
/* .............. */
return (INT_PTR)FALSE;
}
~~~
';
(13):握手对话框
最后更新于:2022-04-01 20:07:33
一提到对话框,相信对它熟悉的人不在少数,更不用说码农们了,你可能会问,对话框和窗口有什么区别吗?本质上是没有区别的,对话框也是一种窗口(前面也说过,控件也可视为子窗口)。
最简单的对话框要数MessageBox弹出来的对话框了,是吧?这个函数我有信心,大家都会用的,毕竟很简单。
好的,废话不多扯了,马上开始本文第一件事,创建一个对话框。
对话框作为一种资源,它存放在资源文件中(.rc),如果项目中没有rc文件,第一种方法是在“解决方案资源管理器”中在“资源文件”节点右击,从菜单中选择“添加”-“新建项”来加入一个rc文件。第二种方法,可以从VS的“视图”菜单中打开“资源视图”,在资源视图中,在项目名节点上右击,从菜单中找到“添加”-“资源”。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d21ca71.png)
然后,选择对话框,点新建按钮。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d22dab4.png)
在属性窗口中为这个对话框名命一个ID,随便你喜欢,我把它设为IDD_MYDLG。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d255efe.png)
OK,现在,我们就可以利用可视化设计器来玩了,看看,还不错的,虽然没有WinForm的设计器那么猛。
把默认两个按钮删掉,我们从工具箱中拖放一些控件。记得为控件的ID命名,就像在WinForm里面要设置Name属性一样。
大概就这样,拖一个Static Text和Button控件,随后我们尝试实现一个功能:点击按钮后,在静态文本框中显示文本。这个编辑器怎么用,就就不说了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d267341.png)
保存资源文件,下面我们开始写代码。
1、在主窗口的消息处理程序中响应WM_CREATE消息,用CreateDialog函数创建并显示非模态对话框。
~~~
LRESULT CALLBACK WinMainProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM LParam)
{
HWND hdlg;
switch (msg)
{
case WM_CREATE:
hdlg = CreateDialog(hgapp,MAKEINTRESOURCE(IDD_MYDLG),hwnd,(DLGPROC)DlgProc);
if(hdlg)
{
//显示对话框
ShowWindow(hdlg, SW_NORMAL);
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd,msg,wParam,LParam);
}
return 0;
}
~~~
CreateDialog的最后一个参数是一个CALLBACK,这个和我们的WindowProc是一鸟样的,注意在定义该函数时,一定要先在头文件或源文件的前面声明一下,不然到这里会找不到,通常我们会把这些函数都放到WinMain函数后面来写,只是通常这样,并不是说一定要这样。
DlgProc如下:
~~~
// 处理对话框中的数据
INT_PTR CALLBACK DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
return (INT_PTR)FALSE;
}
~~~
和WindowProc一样,也有一个DefDlgProc,但是这里最好不要调用,注意MSDN上关于这函数说明的最后一段话。
The DefDlgProc function must not be called by a dialog box procedure; doing so results in recursive execution.
如果在DialogProc中调用DefDlgProc会导致死循环。其实,我们为窗口写的消息循环也是死循环,GetMessage是不断执行,除非接到WM_QUIT消息让它返回假(0)就跳出循环,而对于对话框,我们并没有为它写GetMessage,也不向它PostQuitMessage,它有可能会无法跳出循环。
现在,程序是可以运行的。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d2785a7.PNG)
不过,无论你怎么操作,对话框还是没返应,因为现在我们还没处理相关消息。
当用户操作系统菜单或者点击标题栏的 最大化, 最小化 或 关闭 按钮,都会收到WM_SYSCOMMAND消息,如果不响应WM_SYSCOMMAND,就会放到WM_COMMAND,但WM_COMMAND通常要处理控件的消息,故最好用WM_SYSCOMMAND消息。
2、在对话框的DlgProc中响应WM_SYSCOMMAND。
~~~
case WM_SYSCOMMAND:
if(wParam == SC_CLOSE)
{
// 如果执行了关闭
// 销毁对话框,将收到WM_DESTROY消息
DestroyWindow(hdlg);
}
return 0;
~~~
3、我们已经知道响应按钮单击是处理WM_COMMAND。要改变静态文本中的文本,一可以用 Static_SetText宏,二可以用SetWindowText,三可以发送WM_SETTEXT消息。但是,无论采用哪种方法,我们都得解决一个问题——怎么获取到静态文本控件的句柄。所以,认识一下这个函数:
~~~
HWND WINAPI GetDlgItem(
_In_opt_ HWND hDlg,
_In_ int nIDDlgItem
);
~~~
你猜都猜到了,参数一是对话框的句柄,参数二是要返回句柄的控件的ID。好,我们试试。
~~~
case WM_COMMAND:
{
if(LOWORD(wParam) == IDC_BTN)
{
nCount ++; //每点击一次,就+1
// 获取控件句柄
HWND hStatic = GetDlgItem(hdlg,IDC_DISP);
// 设置控件文本
WCHAR str[MAXCHAR];
// 格式化字符串
int n = wsprintf(str, L"你点击了%d次按钮。", nCount);
//在最后一个字符后加上结止符
str[n] = '\0';
SetWindowText(hStatic, str);
}
}
return 0;
~~~
现在来运行一下,每点击一次按钮,就会在静态文本控件中显示“你点击了X次按钮。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d289a79.PNG)
好,大功告成。
下面是完整的代码清单。
~~~
#include
#include "resource.h"
LRESULT CALLBACK WinMainProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM LParam);
INT_PTR CALLBACK DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam);
HINSTANCE hgapp; //当前应用程序句柄
int WINAPI WinMain(HINSTANCE hThisApp, HINSTANCE hPrevApp, LPSTR lpCmd, int nShow)
{
LPCWSTR cn = L"My";
WNDCLASS wc = {sizeof(WNDCLASS)};
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.hInstance = hThisApp;
wc.lpfnWndProc = WinMainProc;
wc.lpszClassName = cn;
wc.style = CS_HREDRAW | CS_VREDRAW;
//注册窗口类
RegisterClass(&wc);
//创建窗口
HWND hwnd = CreateWindow(cn,L"主窗口",WS_OVERLAPPEDWINDOW,
30,22,360,280,NULL,NULL,hThisApp,NULL);
if(!hwnd)
return 0;
hgapp = hThisApp;
//显示窗口
ShowWindow(hwnd,nShow);
//更新窗口
UpdateWindow(hwnd);
//消息循环
MSG msg;
while (GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
LRESULT CALLBACK WinMainProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM LParam)
{
HWND hdlg;
switch (msg)
{
case WM_CREATE:
hdlg = CreateDialog(hgapp,MAKEINTRESOURCE(IDD_MYDLG),hwnd,(DLGPROC)DlgProc);
if(hdlg)
{
//显示对话框
ShowWindow(hdlg, SW_NORMAL);
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd,msg,wParam,LParam);
}
return 0;
}
// 处理对话框中的数据
INT_PTR CALLBACK DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
static int nCount = 0;//点击按钮的次数
switch(msg)
{
case WM_SYSCOMMAND:
if(wParam == SC_CLOSE)
{
// 如果执行了关闭
// 销毁对话框,将收到WM_DESTROY消息
DestroyWindow(hdlg);
}
return 0;
case WM_COMMAND:
{
if(LOWORD(wParam) == IDC_BTN)
{
nCount ++; //每点击一次,就+1
// 获取控件句柄
HWND hStatic = GetDlgItem(hdlg,IDC_DISP);
// 设置控件文本
WCHAR str[MAXCHAR];
// 格式化字符串
int n = wsprintf(str, L"你点击了%d次按钮。", nCount);
//在最后一个字符后加上结止符
str[n] = '\0';
SetWindowText(hStatic, str);
}
}
return 0;
}
return (INT_PTR)FALSE;
}
~~~
';
(12):使用控件——单选按钮
最后更新于:2022-04-01 20:07:31
今天,咱们还是接着玩“控件斗地主”,这是我原创的超级游戏,有益身心健康,玩一朝,十年少。
哦,对,脑细胞极速运动了一下,想起了一个问题,这个破问题虽然网上有很多种解决方案,但是,并没有让所有人都解决问题。
不知道大家有没有调用过LoadIconMetric函数,这个函数在静态库Comctl32.lib中有定义(当然,动态库也有),不过,创建项目的时候,默认并没有引用它的,于是,大家知道,解决调用的方法就是在代码中加上:
~~~
#pragma comment(lib, "Comctl32.lib")
~~~
我一般习惯这种方法,这样不必去修改项目属性。但是,很多朋友说过,在Win 7以后的系统,依然没有成功,我也尝到了调用失败的“甜头”,我一直在想,这是为什么呢?
于是,我又试了另一种方法,就是用LoadLibrary加载Comctl32.dll,然后通过函数指针去调用它:
~~~
typedef LRESULT (WINAPI * pLoadICMT)(.......);
~~~
但结果还是没成功,GetProcAddress返回的地址为0,又一次尝到了失败带来的“刺激”感。
直到某一天,我在写某程序时,从上一文中大家都看到,那个按钮的视觉风格和Win9x/2000差不多,似乎没有XP那种充满美学水准的效果。其实,这是因为我们的程序没有启用视觉效果,默认情况下,使用版本5中的控件,而要有XP以上的风格,是在版本6的控件内库中才有。
当然方法可以很多人都知道,就是定义一个用于视觉效果的清单文件,本质是XML格式。不过我用的开发工具是VS 2005之后的版本,就不用弄个XML文件那么麻烦了,直接到MSDN上复制粘贴这段代码放到你的代码文件(.cpp)中,就是这个,直接抄过来就行了,适当的时候,要巧用MSDN上的资源。
~~~
// 开启视觉效果 Copy from MSDN
#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
~~~
version是版本号,要有好看的效果,记得要6,不要写用低版本的。processorArchitecture是处理器平台,x86或amd64,用*号最好,通杀。
真是巧啊,原来这么一来,噗,LoadIconMetric也能调用了。总的来说,就是在代码文件加上以下内容:
~~~
#include //包含头文件
// 导入静态库
#pragma comment(lib, "Comctl32.lib")
// 开启视觉效果 Copy from MSDN
#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
~~~
这问题干掉了,开始今天的吹牛大行动。
单选按钮,是的,在WinForm里面你肯定知道,RadioButto,复选框就是CheckBox。不过那个时候.NET还没那么猛,那个时代,就是玩VB6,所以我知道VB里面,单选按钮叫Option吧。
然后找遍了Win32的控件库,怎么没见Radio和CheckBox,于是,我陷入了万分痛苦之中。不久后阅读MSDN文档,我就明白了,其实这两个玩意儿都是BUTTON类的,只是应用了不同的style罢了。
好的,咱们先来弄一个单选的吧。
~~~
case WM_CREATE:
{
CreateWindow(L"Button",L"这玩意儿好",
BS_RADIOBUTTON | WS_CHILD | WS_VISIBLE,
10,10,150,28,
hwnd, (HMENU)IDC_RADBTN1,
(HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE),
NULL);
}
return 0;
~~~
应用BS_RADIOBUTTON样式,就可以使按钮变成单选按钮,不过,别忘了WS_CHILD | WS_VISIBLE。
运行一下。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d1cb270.PNG)
不过,当你点击它的时候,会发现它并不会选中,那是因为BS_RADIOBUTTON样式不会自动让它选上。我们换一种可以自动处理的样式,带AUTO打头的。
就是用 BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE,如下图的预览结果。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d1df489.PNG)
接下来,我们多创建几个吧。
~~~
/*-----------------------------------------------------------------*/
//控件ID
#define IDC_RADBTN1 50001
#define IDC_RADBTN2 50002
#define IDC_RADBTN3 50003
#define IDC_RADBTNBLUE 51001
#define IDC_RADBTNRED 51002
#define IDC_RADBTNGREEN 51003
/*-----------------------------------------------------------------*/
// 消息处理函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_CREATE:
{
// 获取当前实例句柄
HINSTANCE hthisapp = (HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE);
// 纵坐标,控件将以此作为基准,
// 排列时依次增加
int yLoc = 0;
// 用来显示文本
yLoc += 10;
CreateWindow(L"Static",L"请问你的性别是:",
SS_SIMPLE | WS_CHILD | WS_VISIBLE,
10,yLoc,160,18,
hwnd, NULL,
hthisapp,
NULL);
// 第一组单选按钮
yLoc += 22;
CreateWindow(L"Button",L"男",
WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON | WS_GROUP,
12, yLoc, 60, 16,
hwnd,
(HMENU)IDC_RADBTN1,
hthisapp,NULL);
yLoc += 20;
CreateWindow(L"Button",L"女",
WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON,
12,yLoc, 60, 16,
hwnd,(HMENU)IDC_RADBTN2,hthisapp,NULL);
yLoc += 20;
CreateWindow(L"Button",L"人妖",WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON,
12,yLoc,60,16,hwnd,(HMENU)IDC_RADBTN3,hthisapp,NULL);
// 显示文本
yLoc += 38;
CreateWindow(L"Static",L"你喜欢哪一种颜色?",
WS_CHILD | WS_VISIBLE | SS_SIMPLE,
10,yLoc,150,18,hwnd,NULL,hthisapp,NULL);
//第二组单选按钮
yLoc += 22;
CreateWindow(L"Button",L"蓝色",WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON | WS_GROUP,
12,yLoc,60,16,hwnd,(HMENU)IDC_RADBTNBLUE,hthisapp,NULL);
yLoc += 20;
CreateWindow(L"Button",L"红色",WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON,
12,yLoc,60,16,hwnd,(HMENU)IDC_RADBTNRED,hthisapp,NULL);
yLoc += 20;
CreateWindow(L"Button",L"绿色",WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON,
12,yLoc,60,16,hwnd,(HMENU)IDC_RADBTNGREEN,hthisapp,NULL);
}
return 0;
case WM_DESTROY:
PostQuitMessage(0); //平安退出
return 0;
default:
return DefWindowProc(hwnd,msg,wParam,lParam);
}
return 0;
}
~~~
在创建单选框中,为什么有些加了WS_GROUP样式,而有些未加呢?
其实是这样的,既然单选按钮是单选的,那么,任何一个单选按钮都与其他的单选按钮是互斥的关系。所以,在同一个容器(本例是同一个窗口)中就需要把单选按钮进行分组。
同一组中的单选按钮相互排斥,他的分组方法:
顺序,以Tab键顺序为参考,这个不用我介绍,你随便打开一个窗口,然后多按几下Tab键你就懂了,如果不懂,那你真的无可救药了。
凡是设置了WS_GROUP的单选框做为一组中的首元素,随后的所有单选按钮都和它在同一组,直到下一个设置了WS_GROUP样式的单选按钮。用上面的例子来说吧。
性别一组中,第一个应用了WS_GROUP的是“男”,随后的“女”和“人妖”都与“男”在同一组,因为后面一个“蓝色”设置了WS_GROUP样式。所以,
第一组为:男,女,人妖;
第二组为:蓝色,红色,绿色。
显然,用了BS_AUTORADIOBUTTON后,系统就会自动处理选择状态了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d207d29.PNG)
完整的代码清单如下:
~~~
#include
#include
#include //包含头文件
// 导入静态库
#pragma comment(lib, "Comctl32.lib")
// 开启视觉效果 Copy from MSDN
#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
// 先声明一个WindowProc回调
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
// 入口点
int WINAPI wWinMain(HINSTANCE hTheApp, HINSTANCE hPrevApp, LPWSTR lpCmd, int nShow)
{
PCWSTR cn = L"My"; // 窗口名
PCWSTR tt = L"应用程序"; // 窗口标题
// 设计窗口类
WNDCLASS wc = { sizeof(WNDCLASS) };
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpfnWndProc = WindowProc;
wc.style = CS_HREDRAW | CS_VREDRAW;
LoadIconMetric(hTheApp, IDI_APPLICATION, LIM_SMALL, &wc.hIcon);
wc.lpszClassName = cn;
wc.hInstance = hTheApp;
RegisterClass(&wc); // 注册窗口类
// 创建窗口
HWND hwnd = CreateWindow(cn, tt,WS_OVERLAPPEDWINDOW,
28,34,400,330,NULL,NULL,hTheApp,NULL);
if( !hwnd)
{ /* 如果窗口创建失败,
那继续执行也没有意义
长痛不如短痛,结束吧。
*/
return 0;
}
ShowWindow(hwnd,nShow); //显示窗口
UpdateWindow(hwnd); //更新窗口
// 消息循环
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg); //调度消息到WindowProc回调
}
return 0;
}
/*-----------------------------------------------------------------*/
//控件ID
#define IDC_RADBTN1 50001
#define IDC_RADBTN2 50002
#define IDC_RADBTN3 50003
#define IDC_RADBTNBLUE 51001
#define IDC_RADBTNRED 51002
#define IDC_RADBTNGREEN 51003
#define IDC_BTN_OK 1107 //确定按钮ID
/*-----------------------------------------------------------------*/
// 消息处理函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_CREATE:
{
// 获取当前实例句柄
HINSTANCE hthisapp = (HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE);
// 纵坐标,控件将以此作为基准,
// 排列时依次增加
int yLoc = 0;
// 用来显示文本
yLoc += 10;
CreateWindow(L"Static",L"请问你的性别是:",
SS_SIMPLE | WS_CHILD | WS_VISIBLE,
10,yLoc,160,18,
hwnd, NULL,
hthisapp,
NULL);
// 第一组单选按钮
yLoc += 22;
CreateWindow(L"Button",L"男",
WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON | WS_GROUP,
12, yLoc, 60, 16,
hwnd,
(HMENU)IDC_RADBTN1,
hthisapp,NULL);
yLoc += 20;
CreateWindow(L"Button",L"女",
WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON,
12,yLoc, 60, 16,
hwnd,(HMENU)IDC_RADBTN2,hthisapp,NULL);
yLoc += 20;
CreateWindow(L"Button",L"人妖",WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON,
12,yLoc,60,16,hwnd,(HMENU)IDC_RADBTN3,hthisapp,NULL);
// 显示文本
yLoc += 38;
CreateWindow(L"Static",L"你喜欢哪一种颜色?",
WS_CHILD | WS_VISIBLE | SS_SIMPLE,
10,yLoc,150,18,hwnd,NULL,hthisapp,NULL);
//第二组单选按钮
yLoc += 22;
CreateWindow(L"Button",L"蓝色",WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON | WS_GROUP,
12,yLoc,60,16,hwnd,(HMENU)IDC_RADBTNBLUE,hthisapp,NULL);
yLoc += 20;
CreateWindow(L"Button",L"红色",WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON,
12,yLoc,60,16,hwnd,(HMENU)IDC_RADBTNRED,hthisapp,NULL);
yLoc += 20;
CreateWindow(L"Button",L"绿色",WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON,
12,yLoc,60,16,hwnd,(HMENU)IDC_RADBTNGREEN,hthisapp,NULL);
// 创建一个确定按钮
CreateWindow(L"Button",L"确定",WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
230,180,80,27,hwnd,(HMENU)IDC_BTN_OK,hthisapp,NULL);
}
return 0;
case WM_DESTROY:
PostQuitMessage(0); //平安退出
return 0;
default:
return DefWindowProc(hwnd,msg,wParam,lParam);
}
return 0;
}
~~~
';
(11):使用控件——先来耍一下按钮
最后更新于:2022-04-01 20:07:29
用户通过控件与应用程序交互,在吹牛之前,先介绍一个工具,这是官方的工具,使用它,你可以预览常用控件的外观、样式,以及对控进行操作时接收和发送哪些消息。下载地址如下:
[http://www.microsoft.com/en-us/download/details.aspx?id=4635](http://www.microsoft.com/en-us/download/details.aspx?id=4635)
我们可以把控件当成特殊的一类窗口,所以,创建控件与创建窗口一样,使用CreateWindow或CreateWindowEx函数,不过,在窗口样式上面记得用上以下两位帅哥:
a、WS_CHILD:控件是放在我们的窗口上的,自然要作为窗口的子窗口,WS_CHILDWINDOW也一样,为了节约几个字母,用WS_CHILD吧。
b、WS_VISIBLE:既然要使用控件,自然要让别人看得见。要想别人称赞你老婆漂亮,当然要先让别人见一见你老婆了,哈哈,不要想歪了。
理论的东西,怎么说都是抽象的,咱们还是“所见即所得”吧。那么,到底在啥时候创建控件合适一点呢?一种方法是在WinMain方法中创建,把CreateWindow函数的hWndParent设置为窗口的句柄。
俺这里用第二种方法,我们知道,在窗口创建后,显示之前,即CreateWindow函数返回之前,我们会收到WM_CREATE消息,我们响应它的号召,艰苦奋斗创建一个按钮。
~~~
case WM_CREATE:
{
//创建按钮
HWND hButton = CreateWindow(L"Button", L"有种就来点击我!", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
35, 45, 160, 65, hwnd, NULL, hg_app, NULL);
}
return 0;
~~~
hg_app是我定义的一个全局变量,当前应用程序的句柄(HINSTANCE类型)。
然后,运行一下,先看看效果再说吧。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d18e02c.PNG)
接下来,新的问题来了,按钮我是创建了,但怎么响应用户点击呢?其实,这按钮与菜单项一样,单击用户与她“亲密”接触后,我们的WindowProc会收到WM_COMMAND消息,和菜单一样。
wParam的低字节位表示ID号,高字节位表示控件通知,比如用户单击了按钮,通知码为BN_CLICKED,这样我们就可以了解到用户具体对按钮干了什么。
lParam中保存了控件的句柄。
问题是,怎么设置控件的ID?我们看看CreateWindow的文档介绍。
~~~
HWND WINAPI CreateWindow(
_In_opt_ LPCTSTR lpClassName,
_In_opt_ LPCTSTR lpWindowName,
_In_ DWORD dwStyle,
_In_ int x,
_In_ int y,
_In_ int nWidth,
_In_ int nHeight,
_In_opt_ HWND hWndParent,
_In_opt_ HMENU hMenu,
_In_opt_ HINSTANCE hInstance,
_In_opt_ LPVOID lpParam
);
~~~
hMenu [in, optional]
Type: HMENU
A handle to a menu, or specifies a child-window identifier depending on the window style. For an overlapped or pop-up window, hMenu identifies the menu to be used with the window; it can be NULL if the class menu is to be used. For a child window, hMenu specifies the child-window identifier, an integer value used by a dialog box control to notify its parent about events. The application determines the child-window identifier; it must be unique for all child windows with the same parent window.
简单地说,控件通常不需要菜单,所以,可以用这个参数来设置控件的ID,反正hMenu闲着也没事干,就给个ID它玩玩。ID号是一个整数,不过为了可读性,一般是声明一个宏,其实我们在资源编辑器中使用的资源ID(如IDM_FUCK)就是在resource.h中定义的宏的,既然叫ID了,你就知道它的值不要重复。
我们也来模拟一下,在文件的前面(#include...后)也声明三个宏,分别标识三个按钮。
~~~
#define IDB_ONE 3301
#define IDB_TWO 3302
#define IDB_THREE 3303
~~~
然后创建三个按钮:
~~~
case WM_CREATE:
{
//创建三个按钮
CreateWindow(L"Button", L"按钮一", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
35, 10, 120, 60, hwnd, (HMENU)IDB_ONE, hg_app, NULL);
CreateWindow(L"Button", L"按钮二", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
35, 80, 120, 60, hwnd, (HMENU)IDB_TWO, hg_app, NULL);
CreateWindow(L"Button", L"按钮三", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
35, 150, 120, 60, hwnd, (HMENU)IDB_THREE, hg_app, NULL);
}
return 0;
~~~
然后我们来响应WM_COMMAND消息。
~~~
case WM_COMMAND:
{
switch(LOWORD(wParam))
{
case IDB_ONE:
MessageBox(hwnd, L"您点击了第一个按钮。", L"提示", MB_OK | MB_ICONINFORMATION);
break;
case IDB_TWO:
MessageBox(hwnd, L"您点击了第二个按钮。", L"提示", MB_OK | MB_ICONINFORMATION);
break;
case IDB_THREE:
MessageBox(hwnd, L"您点击了第三个按钮。", L"提示", MB_OK | MB_ICONINFORMATION);
break;
default:
break;
}
}
return 0;
~~~
看看效果。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d1a44f1.PNG)
这时候,我希望,当我点击了按钮后,按钮上的文本变成“按钮X已点击”,该怎么做呢?Windows系统是基于消息机制的,所以,首先想到,向控件发送消息,要改变控件相关的文本,应当发送WM_SETTEXT消息。
我们把上面的代码改一下。
~~~
case WM_COMMAND:
{
switch(LOWORD(wParam))
{
case IDB_ONE:
//MessageBox(hwnd, L"您点击了第一个按钮。", L"提示", MB_OK | MB_ICONINFORMATION);
SendMessage((HWND)lParam, WM_SETTEXT, (WPARAM)NULL, (LPARAM)L"第一个按鈕已点击");
break;
case IDB_TWO:
//MessageBox(hwnd, L"您点击了第二个按钮。", L"提示", MB_OK | MB_ICONINFORMATION);
SendMessage((HWND)lParam, WM_SETTEXT, (WPARAM)NULL, (LPARAM)L"第二个按鈕已点击");
break;
case IDB_THREE:
//MessageBox(hwnd, L"您点击了第三个按钮。", L"提示", MB_OK | MB_ICONINFORMATION);
SendMessage((HWND)lParam, WM_SETTEXT, (WPARAM)NULL, (LPARAM)L"第三个按鈕已点击");
break;
default:
break;
}
}
return 0;
~~~
前面我们知道,WM_COMMAND消息的lParam保存控件的句柄,所以,我们传给SendMessage的第一个参数是操作目标的句柄,注意,这里不要传WindowProc回调中的参数,因为我们现在要操作的对象是按钮,不是窗口,WindowProc传进到的句柄是指我们注册的窗口,因为我们在WNDCLASS中已经设定了该WindowProc函数。
要对按钮进行操作,应当使用WM_COMMAND的lParam中包含的值,强制转换为HWND。
运行结果如下图所示。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d1b74b1.PNG)
完整的示例如下:
~~~
#include
#include
//#include "resource.h"
#define IDB_ONE 3301
#define IDB_TWO 3302
#define IDB_THREE 3303
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
LPCWSTR lps_cl = L"MyApp";//类名
LPCWSTR wd_text = L"超级应用";//窗口标题
HINSTANCE hg_app;//全局实例句柄
int WINAPI wWinMain(HINSTANCE hThisApp,
HINSTANCE hPrevApp,
LPWSTR lpCmd,
int nShow)
{
WNDCLASSEX wc = { };
wc.cbSize = sizeof(WNDCLASSEX);
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.hInstance = hThisApp;
wc.lpfnWndProc = (WNDPROC)WindowProc;
wc.lpszClassName = lps_cl;
wc.style = CS_HREDRAW | CS_VREDRAW;
RegisterClassEx(&wc);
HWND hwnd = CreateWindowEx(WS_EX_WINDOWEDGE,
lps_cl,
wd_text,
WS_OVERLAPPEDWINDOW,
20,
25,
400,
300,
NULL,
NULL,
hThisApp,
NULL);
if(hwnd == NULL)
return -1;
ShowWindow(hwnd, nShow);
UpdateWindow(hwnd);
hg_app = hThisApp;
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_CREATE:
{
//创建三个按钮
CreateWindow(L"Button", L"按钮一", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
35, 10, 160, 60, hwnd, (HMENU)IDB_ONE, hg_app, NULL);
CreateWindow(L"Button", L"按钮二", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
35, 80, 160, 60, hwnd, (HMENU)IDB_TWO, hg_app, NULL);
CreateWindow(L"Button", L"按钮三", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
35, 150, 160, 60, hwnd, (HMENU)IDB_THREE, hg_app, NULL);
}
return 0;
case WM_COMMAND:
{
switch(LOWORD(wParam))
{
case IDB_ONE:
//MessageBox(hwnd, L"您点击了第一个按钮。", L"提示", MB_OK | MB_ICONINFORMATION);
SendMessage((HWND)lParam, WM_SETTEXT, (WPARAM)NULL, (LPARAM)L"第一个按鈕已点击");
break;
case IDB_TWO:
//MessageBox(hwnd, L"您点击了第二个按钮。", L"提示", MB_OK | MB_ICONINFORMATION);
SendMessage((HWND)lParam, WM_SETTEXT, (WPARAM)NULL, (LPARAM)L"第二个按鈕已点击");
break;
case IDB_THREE:
//MessageBox(hwnd, L"您点击了第三个按钮。", L"提示", MB_OK | MB_ICONINFORMATION);
SendMessage((HWND)lParam, WM_SETTEXT, (WPARAM)NULL, (LPARAM)L"第三个按鈕已点击");
break;
default:
break;
}
}
return 0;
default:
return DefWindowProc(hwnd,msg,wParam,lParam);
}
return 0;
}
~~~
';
(10):绘图(C)
最后更新于:2022-04-01 20:07:26
今天我们来欣赏一下用于填充图形的函数,当然我不会逐个去介绍,因为我们参考MSDN直接套参数就可以了。
SetDCBrushColor函数有必要扯一下,它的声明如下:
~~~
COLORREF SetDCBrushColor(
__in HDC hdc,
__in COLORREF crColor
);
~~~
第二个参数,通过RGB宏产生COLORREF传进去就可以了,比如这样:
~~~
SetDCBrushColor(ps.hdc,RGB(211,254,41));
~~~
但是,如果只是调用这个函数,你会发现在绘图的时候,画刷的颜色还是没有变化,因为我们还没有将HBRUSH的默认画刷DC_BRUSH选到DC中去。所以,在调用SetDCBrushColor之前,要把默认的画刷先放到设备上下文,默认画刷可以通过GetStockObject(DC_BRUSH)获得。
~~~
SelectObject(ps.hdc,GetStockObject(DC_BRUSH));
~~~
接下来我们可以尝试填充几个图形试试,如矩形、椭圆、饼图等。
~~~
case WM_PAINT:
{
BeginPaint(hwnd,&ps);
SelectObject(ps.hdc,GetStockObject(DC_BRUSH));
SetDCBrushColor(ps.hdc,RGB(0,0,255));
Rectangle(ps.hdc,20,18,68,50);
SetDCBrushColor(ps.hdc,RGB(220,32,70));
Rectangle(ps.hdc,125,100,230,300);
SetDCBrushColor(ps.hdc,RGB(30,235,12));
Ellipse(ps.hdc,270,80,390,223);
SetDCBrushColor(ps.hdc,RGB(35,160,242));
Chord(ps.hdc,185,260,420,480,190,260,405,479);
SetDCBrushColor(ps.hdc,RGB(211,254,41));
Pie(ps.hdc,35,320,300,600,56,470,60,360);
EndPaint(hwnd,&ps);
}
return 0;
~~~
每一次调用SetDCBrushColor都会改变画刷的颜色,所以,比如你希望绘制蓝色的矩形,在调用Rectangle之前就要调用SetDCBrushColor修改画刷颜色,然后再画矩形。我们可以看看上面代码的最终效果。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d120fc0.PNG)
下面,我们做一个人类历史上最简单的画图程序。
我们为程序提供几种可选的线条风格,通过菜单来选择,如实线,虚线等,鼠标按下左键后开始,鼠标左键弹起就完成一条直线的绘制。为了简化,我们把相应菜单的ID设置的值与CreatePen中的线型的宏的值一致。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d1548c5.PNG)
这样一来,选择了哪个菜单就直接用这个菜单的ID来创建画笔,就省去了许多代码。
在响应WM_CREATE消息时,创建菜单。
~~~
case WM_CREATE:
{
// 创建菜单
HMENU menubar = CreateMenu();
HMENU menuPop = CreatePopupMenu();
AppendMenu(menuPop,MF_STRING,(UINT_PTR)PS_SOLID,L"实线");
AppendMenu(menuPop,MF_STRING,(UINT_PTR)PS_DASH,L"虚线");
AppendMenu(menuPop,MF_STRING,(UINT_PTR)PS_DOT,L"点线");
AppendMenu(menuPop,MF_STRING,(UINT_PTR)PS_DASHDOT,L"点虚线");
AppendMenu(menubar, MF_STRING | MF_POPUP, (UINT_PTR)menuPop, L"选择线型");
SetMenu(hwnd, menubar);
}
return 0;
~~~
现在我们来想一下,绘制直线的大概思路。
1、鼠标左键按下,记录线条的起点。
2、鼠标左键弹起时,记录线条的终点,并画出整条线。
3、当窗口发生重绘时,前面画的所有线条被清除,要希望保留前面画的线条,就要响应WM_PAINT消息,把所有线条重新画一次。
4、由于我们会在窗口上画出多条线,程序需要定义一个结构体用来保存线条的起点、终点和所使用的线型。
5、正因为需要保存多条线的数据,故可以把每一条线的相关数据放到一个vector中。
根据上面的分析,完成程序的代码如下:
~~~
#include
#include
#include
using namespace std;
typedef struct tagData
{
int ptBeginX, ptBeginY;//起点
int ptEndX, ptEndY;//终点
int penStyle;//画笔的线型
} PAINTDATA;
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(
HINSTANCE hThisApp,
HINSTANCE hPrevApp,
LPSTR lpsCmd,
int nShow)
{
WNDCLASS wc = {};
wc.hbrBackground = CreateSolidBrush(RGB(0,0,0));
wc.hInstance = hThisApp;
wc.lpfnWndProc = WindowProc;
wc.lpszClassName = L"My";
wc.style = CS_HREDRAW | CS_VREDRAW;
RegisterClass(&wc);
HWND hwnd = CreateWindow(
L"My",
L"应用程序",
WS_OVERLAPPEDWINDOW,
50,
20,
600,
480,
NULL,
NULL,
hThisApp,
NULL);
if(hwnd == NULL)
return -1;
ShowWindow(hwnd, nShow);
UpdateWindow(hwnd);
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
static vector datas;
static int penStyle = PS_SOLID;
static PAINTDATA *pCurrentData = NULL;//指向当前PAINTDATA的指针
switch(msg)
{
case WM_CREATE:
{
// 创建菜单
HMENU menubar = CreateMenu();
HMENU menuPop = CreatePopupMenu();
AppendMenu(menuPop,MF_STRING,(UINT_PTR)PS_SOLID,L"实线");
AppendMenu(menuPop,MF_STRING,(UINT_PTR)PS_DASH,L"虚线");
AppendMenu(menuPop,MF_STRING,(UINT_PTR)PS_DOT,L"点线");
AppendMenu(menuPop,MF_STRING,(UINT_PTR)PS_DASHDOT,L"点虚线");
AppendMenu(menubar, MF_STRING | MF_POPUP, (UINT_PTR)menuPop, L"选择线型");
SetMenu(hwnd, menubar);
}
return 0;
case WM_COMMAND:
{
//为选中的菜单加上单选标记
HMENU mnbar = GetMenu(hwnd);
HMENU hmnPop = GetSubMenu(mnbar, 0);
CheckMenuRadioItem(hmnPop, PS_SOLID, PS_DASHDOT, LOWORD(wParam), MF_BYCOMMAND);
//记录用户选择的线型
penStyle = (int)LOWORD(wParam);
}
return 0;
case WM_LBUTTONDOWN:
{
pCurrentData = new PAINTDATA;
//获取起点
pCurrentData ->penStyle = penStyle;
pCurrentData->ptBeginX = GET_X_LPARAM(lParam);
pCurrentData->ptBeginY = GET_Y_LPARAM(lParam);
}
return 0;
case WM_LBUTTONUP:
{
if(pCurrentData != NULL)
{
//获取终点
pCurrentData->ptEndX = GET_X_LPARAM(lParam);
pCurrentData->ptEndY = GET_Y_LPARAM(lParam);
//画出线条
HDC hdc = GetDC(hwnd);
HPEN pen = CreatePen(pCurrentData->penStyle,1,RGB(0,255,0));
HPEN oldpen = (HPEN)SelectObject(hdc,pen);
MoveToEx(hdc,pCurrentData->ptBeginX,pCurrentData->ptBeginY,NULL);
LineTo(hdc,pCurrentData->ptEndX,pCurrentData->ptEndY);
SelectObject(hdc,oldpen);
DeleteObject(pen);
ReleaseDC(hwnd,hdc);
//把当前数据添加到vector中
datas.push_back(*pCurrentData);
}
}
return 0;
case WM_PAINT:
{
PAINTSTRUCT ps;
BeginPaint(hwnd,&ps);
//将所有线条重新画一遍
vector::const_iterator item;
for(item = datas.begin(); item != datas.end(); item++)
{
HPEN pen = CreatePen(item->penStyle, 1, RGB(0,255,0));
SelectObject(ps.hdc, pen);
MoveToEx(ps.hdc, item->ptBeginX, item->ptBeginY, NULL);
LineTo(ps.hdc, item->ptEndX, item->ptEndY);
DeleteObject(pen);
}
EndPaint(hwnd,&ps);
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd,msg,wParam,lParam);
}
return 0;
}
~~~
结构体PAINTDATA用来保存每一条线的起点坐标、终点坐标、线型。为了避免在跳出WindowProc后所有数据被回收,可以使用static关键字来声明变量,这样这些变量的生命周期就与整个应用程序相同了。
运行程序后,在菜单中选择一种线型,然后在窗口上画线,效果如图所示。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d168df8.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d17cd18.PNG)
';
(9):绘图(B)
最后更新于:2022-04-01 20:07:24
我们今天继续涂鸦,实践证明,涂鸦是人生一大乐趣。
首先,我们写一个程序骨架子,以便做实验。
~~~
#include
LRESULT CALLBACK MainWinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(
HINSTANCE hThisApp,
HINSTANCE hPrevApp,
LPSTR lpsCmdln,
int iShow)
{
WNDCLASS wc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hbrBackground = CreateSolidBrush(RGB(0,0,0));
// 默认光标类型为箭头
wc.hCursor = LoadCursor(hThisApp, IDC_ARROW);
// 默认应用程序图标
wc.hIcon = LoadIcon(hThisApp, IDI_APPLICATION);
wc.hInstance = hThisApp;
wc.lpfnWndProc = MainWinProc;
wc.lpszClassName = L"MyAppTest";
wc.lpszMenuName = NULL;
wc.style = CS_HREDRAW | CS_VREDRAW;
// 注册窗口类
RegisterClass(&wc);
// 创建窗口
HWND hwnd = CreateWindow(
L"MyAppTest",
L"绘画课",
/* 使用 WS_VISIBLE 就不用调用ShowWindow了 */
WS_VISIBLE | WS_OVERLAPPEDWINDOW,
100,
45,
500,
380,
NULL,
NULL,
hThisApp,
NULL);
// 消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
LRESULT CALLBACK MainWinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
PAINTSTRUCT ps;
BeginPaint(hwnd, &ps);
/*
待实现
*/
EndPaint(hwnd, &ps);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
~~~
### CreatePen函数
我们要进行素描画创作,所以我们必须想清楚要使用什么样的钢笔,画出什么样的线条。故,画图之前得创建一支钢笔,不然,巧妇难为无米之炊。
~~~
HPEN CreatePen(
int iStyle, //钢笔的样式,如虚线、实线
int cWidth, //线条宽度
COLORREF color //线条是啥颜色的
);
~~~
第一个参数指定线条的样式,如
~~~
/* Pen Styles */
#define PS_SOLID 0
#define PS_DASH 1 /* ------- */
#define PS_DOT 2 /* ....... */
#define PS_DASHDOT 3 /* _._._._ */
#define PS_DASHDOTDOT 4 /* _.._.._ */
#define PS_NULL 5
#define PS_INSIDEFRAME 6
#define PS_USERSTYLE 7
#define PS_ALTERNATE 8
~~~
函数成功创建钢笔后就会返回HPEN,H开头的你就要知道它表示句柄。Pen也是系统资源,所以创建笔后系统要为它一个标识。
### SelectObject函数
上过美术课,你会知道,有了用于进行素描创作的钢笔还不能动手干活,我们还需要有纸。接下来,调用SelectObject函数,把刚才创建的钢笔与绘图纸关联,就等于把我们创建的绘图资源放进DC工具箱中,
~~~
HGDIOBJ WINAPI SelectObject(
HDC hdc, //设备描述表的句柄
HGDIOBJ h //要放到DC中的资源的句柄
);
~~~
调用成功后,返回原先的资源句柄。
### MoveToEx和LineTo
MoveToEx是设置绘制的起点,下次绘图将从这个点开始。它的最后一个参数将被设置为当前点,即Move后的坐标。LineTo从当前坐标开始,到指定坐标之间绘制一条线段。
~~~
// 创建钢笔
HPEN pen = CreatePen(PS_DASH, 1, RGB(0,255,50));
// 把笔选到DC中
SelectObject(ps.hdc, pen);
// 设定线段的起点
MoveToEx(ps.hdc, 15, 25, NULL);
// 绘制线条
LineTo(ps.hdc, 65, 49);
LineTo(ps.hdc, 12, 120);
LineTo(ps.hdc, 250, 78);
LineTo(ps.hdc, 312, 185);
DeleteObject(pen);
~~~
记住,只要是我们创建的句柄,用完后调用DeleteObject函数将其销毁。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d0d35f8.PNG)
### PolyBezier函数和PolyBezierTo函数
两个函数都是用来绘制贝塞尔曲线的,不同的是,PolyBezier函数包含指定的起点和终点,PolyBezierTo是从当前点开始绘制贝塞尔曲线。
~~~
// 绘制贝塞尔曲线
pen = CreatePen(PS_DOT, 1, RGB(0,3,255));
SelectObject(ps.hdc, pen);
POINT* pts = new POINT[4];
pts[0].x = 421;
pts[0].y = 16;
pts[1].x = 7;
pts[1].y = 197;
pts[2].x = 480;
pts[2].y = 320;
pts[3].x = 30;
pts[3].y = 350;
PolyBezier(ps.hdc, pts, 4);
delete [] pts;
// 第二段贝塞尔曲线
POINT* pts2 = new POINT[3];
pts2[0].x = 176;
pts2[0].y = 84;
pts2[1].x = 17;
pts2[1].y = 247;
pts2[2].x = 400;
pts2[2].y = 490;
// 移动当前点
MoveToEx(ps.hdc, 395, 270, NULL);
PolyBezierTo(ps.hdc, pts2, 3);
delete [] pts2;
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d0e68ff.PNG)
###
### PolyPolyline绘制复合线条
PolyPolyline函数可以绘制多段复合线条。它的声明如下:
~~~
BOOL PolyPolyline(
HDC hdc,
const POINT *lppt,
const DWORD *lpdwPolyPoints,
DWORD cCount
);
~~~
lppt参数指向一个POINT的数组,它包含绘制复合线条所需的所有点;lpdwPolyPoints指向一个数组,这个数字数组指明如何分配点数组。
例如,lppt有7个点,我计划,前2个点绘制第一条线,接着3个点绘制第二条线,最后2个点绘制第三条线,这样一来,lpdwPolyPolyPoints得值应为:
{ 2, 3, 2 }
nCount里包含lpdwPolyPolyPoints中的数量,我们上面的例子是3.
由于两点决定一条线段,因此,lpdwPolyPolyPoints里面的值记得要>=2。
~~~
// 复杂图形
pen = CreatePen(PS_DASHDOTDOT, 1, RGB(80,20,160));
SelectObject(ps.hdc, pen);
POINT plpts[10] =
{
{47,3}, {11,46}, {28,199}, {203,305}, {94,22},
{402,377}, {21,45}, {237,7}, {300,398}, {175,25}
};
DWORD arr[4] = { 2, 3, 3, 2};
PolyPolyline(ps.hdc, &plpts[0], &arr[0], 4);
~~~
上面的代码将画出以下图形。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d10c0f7.PNG)
完整的代码清单如下:
~~~
#include
LRESULT CALLBACK MainWinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(
HINSTANCE hThisApp,
HINSTANCE hPrevApp,
LPSTR lpsCmdln,
int iShow)
{
WNDCLASS wc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hbrBackground = CreateSolidBrush(RGB(0,0,0));
// 默认光标类型为箭头
wc.hCursor = LoadCursor(hThisApp, IDC_ARROW);
// 默认应用程序图标
wc.hIcon = LoadIcon(hThisApp, IDI_APPLICATION);
wc.hInstance = hThisApp;
wc.lpfnWndProc = MainWinProc;
wc.lpszClassName = L"MyAppTest";
wc.lpszMenuName = NULL;
wc.style = CS_HREDRAW | CS_VREDRAW;
// 注册窗口类
RegisterClass(&wc);
// 创建窗口
HWND hwnd = CreateWindow(
L"MyAppTest",
L"绘画课",
/* 使用 WS_VISIBLE 就不用调用ShowWindow了 */
WS_VISIBLE | WS_OVERLAPPEDWINDOW,
100,
45,
500,
380,
NULL,
NULL,
hThisApp,
NULL);
// 消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
LRESULT CALLBACK MainWinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
PAINTSTRUCT ps;
BeginPaint(hwnd, &ps);
// 创建钢笔
HPEN pen = CreatePen(PS_DASH, 1, RGB(0,255,50));
// 把笔选到DC中
SelectObject(ps.hdc, pen);
// 设定线段的起点
MoveToEx(ps.hdc, 15, 25, NULL);
// 绘制线条
LineTo(ps.hdc, 65, 49);
LineTo(ps.hdc, 12, 120);
LineTo(ps.hdc, 250, 78);
LineTo(ps.hdc, 312, 185);
// 绘制贝塞尔曲线
pen = CreatePen(PS_DOT, 1, RGB(0,3,255));
SelectObject(ps.hdc, pen);
POINT* pts = new POINT[4];
pts[0].x = 421;
pts[0].y = 16;
pts[1].x = 7;
pts[1].y = 197;
pts[2].x = 480;
pts[2].y = 320;
pts[3].x = 30;
pts[3].y = 350;
PolyBezier(ps.hdc, pts, 4);
delete [] pts;
// 第二段贝塞尔曲线
POINT* pts2 = new POINT[3];
pts2[0].x = 176;
pts2[0].y = 84;
pts2[1].x = 17;
pts2[1].y = 247;
pts2[2].x = 400;
pts2[2].y = 490;
// 移动当前点
MoveToEx(ps.hdc, 395, 270, NULL);
PolyBezierTo(ps.hdc, pts2, 3);
delete [] pts2;
// 复杂图形
pen = CreatePen(PS_DASHDOTDOT, 1, RGB(80,20,160));
SelectObject(ps.hdc, pen);
POINT plpts[10] =
{
{47,3}, {11,46}, {28,199}, {203,305}, {94,22},
{402,377}, {21,45}, {237,7}, {300,398}, {175,25}
};
DWORD arr[4] = { 2, 3, 3, 2};
PolyPolyline(ps.hdc, &plpts[0], &arr[0], 4);
DeleteObject(pen);
EndPaint(hwnd, &ps);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
~~~
';
(8):绘图(A)
最后更新于:2022-04-01 20:07:22
从本篇开始,我就不吹牛皮,那就吹吹兔皮吧。说说与绘图有关的东东。
要进行绘制,首先要得到一个DC,啥是DC呢?按字面翻译叫设备上下文,也可以翻译为设备描述表,它主要指API为我们封装了一些与显示设备相关的交互操作,我们这里说的是图形的绘制,自然指的是显卡。当然,对于同一客观事物,世界上并不存在唯一的理解方案,技术上的东西最终拿来用的,不应该有硬性的去统一。我们之中的很多人,最大的失败在于,人家说要这样理解他就毫不怀疑地这样理解,权威人士说要这样这样,他就不经过大脑思考地跟着那样那样。
虽然我的母校是名不见经传的三流大学,但回忆我的大学,很幸运,我曾经遇到几位好老师,真正的好老师,不是那些所谓的叫兽砖家。记得某位老师说过:这本书,如果读完了你一无所获,那你太失败了;如果你把书中的内容都掌握了,勉强及格;如果你能把书中的所有观点全部推翻,你才是优秀的。
在许多情况下,我们绘图都是遵循先GetDC-----〉绘图------〉ReleaseDC,DC是一种资源,用完了要释放,我们到图书馆借书,看完了要还书。不过,在处理WM_PAINT消息时,调用BeginPaint函数后,开始绘图,画完了调用EndPaint。当然这个并不违背我们前面所说的使用完HDC要释放的道理,只是BeginPaint函数会自动调用GetDC,EndPaint会自动调用ReleaseDC。
好的,首先我们来写几个字吧。绘制文本可以使用DrawText函数,他的最后一个参数是文本的对齐格式,如左对齐、居中、右对齐等。
~~~
PAINTSTRUCT ps;
switch(msg)
{
case WM_PAINT:
BeginPaint(hwnd, &ps);
~~~
声明一个PAINTSTRUCT结构体的变量,然后传给BeginPaint函数,之后就可以画东西了。
~~~
DrawText(ps.hdc,L"床前明月光", -1, &rect, DT_CENTER);
~~~
但是,如果我希望文本的颜色不是默认的黑色,我们可以考虑调用SetTextColor函数来设置颜色,之后我们绘制的所有文本都是这个颜色了,如果之后希望改变文本的颜色,就再次调用SetTextColor函数。
~~~
SetTextColor(ps.hdc, RGB(0,150,255));
~~~
RGB宏可以通过三个值来确定颜色值,这个估计不用我介绍了,如果不懂RGB,可以去请教芙蓉姐姐。
我希望新绘制的文本在前一个文本的下一行,当然,你可能会说,用DrawText的时候把传给它的RECT改一下坐标就行了。这方法虽然可以,但我们不好调坐标。其实,我们如果知道文本字符的高度,那不就好办了吗,对的,要获得文本高度,可以调用GetTextMetrics函数。现在我们要用的工具都齐全了。
~~~
case WM_PAINT:
BeginPaint(hwnd, &ps);
TEXTMETRIC tm;
// 取得与文本相关的数据
GetTextMetrics(ps.hdc, &tm);
RECT rect;
rect.top = 0L;
rect.left = ps.rcPaint.left;
rect.right = ps.rcPaint.right;
rect.bottom = rect.top + tm.tmHeight;
// 第一行文本
SetTextColor(ps.hdc, RGB(0,150,255));
DrawText(ps.hdc,L"床前明月光", -1, &rect, DT_CENTER);
// 第二行文本
rect.top += tm.tmHeight;
rect.bottom += tm.tmHeight;
SetTextColor(ps.hdc, RGB(220, 12, 50));
DrawText(ps.hdc, L"疑是地上霜", -1, &rect, DT_LEFT);
// 第三行文本
rect.top += tm.tmHeight;
rect.bottom += tm.tmHeight;
SetTextColor(ps.hdc, RGB(30,255,7));
DrawText(ps.hdc, L"举头望明月", -1, &rect, DT_RIGHT);
// 第四行文本
rect.top += tm.tmHeight;
rect.bottom += tm.tmHeight;
SetTextColor(ps.hdc, RGB(0,40,210));
DrawText(ps.hdc, L"低头思故乡", -1, &rect, DT_RIGHT);
EndPaint(hwnd, &ps);
return 0;
~~~
这个不难理解吧,就是每一行文本的矩形区域得顶部和底部坐标分别加上文本的高度。
现在可以看看效果了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d089203.PNG)
接下来,我们画几条弧线。绘制弧线使用Arc函数,第一个参数是目标HDC,随后的4个参数用于确定弧线所在的位置的矩形,最后4个参数是确定弧线的开始点和结束点的坐标。
~~~
BOOL WINAPI Arc(
HDC hdc, //DC的句柄
int x1, // 矩形的左坐标
int y1, //矩形上坐标
int x2,//矩形的右坐标
int y2, //矩形的下坐标
int x3, //起点x坐标
int y3, //起点y坐标
int x4, //终点x坐标
int y4 //终点y坐标
);
~~~
在默认情况下,弧线是逆时针方向的。
~~~
// 绘制弧线
HPEN pen = CreatePen(PS_SOLID, 2, RGB(200, 100, 20));//创建笔
// 将笔选到DC中
auto oldObj = SelectObject(ps.hdc, pen);
// 画弧线
Arc(ps.hdc, 20, 100, 300, 300, 39, 110, 280, 285);
Arc(ps.hdc, 200, 160, 390, 400, 300,350, 380,165);
// 画完之后,把原先的笔选回去
SelectObject(ps.hdc, oldObj);
// 清理
DeleteObject(pen);
~~~
上面代码将画出如下图所示的弧线。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d09c1b1.PNG)
默认是逆时针方向,现在我想画顺时针方向的弧线。看下面例子,通过SetArcDirection函数改变弧线的方向。
~~~
/*
AD_COUNTERCLOCKWISE表示逆时针方向
AD_CLOCKWISE表示顺时针方向
*/
SetArcDirection(ps.hdc, AD_CLOCKWISE);
Arc(ps.hdc, 20,150, 460,450, 90,162, 85,300);
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d0ac503.PNG)
下面介绍一下LineDDA函数,这个家伙不简单,为啥?因为I它可以通过回调函数来对一条线段中不同的点进行分别处理。其回调函数如下:
~~~
VOID CALLBACK LineDDAProc(int x, int y, LPARAM lpData);
~~~
最后一个参数是长指针,我们可以将一个HDC的地址传给它。
因为需要回调函数,我们得先写好回调函数,但是,在文件的前面要先声明一下,C语言的函数如果在调用之后定义,就必须先声明,不然编译的时候找不到。
~~~
VOID CALLBACK LineDDAProc(int x, int y, LPARAM lpData)
{
// 从参数中取得HDC
HDC hdc = *((HDC*)lpData);
// 不同位置的线段设置不同的颜色
int type=0;
if(x <= 510 || y <= 200)
{
type = 0;
}
else if((x > 510 && x <= 700) ||
(y > 720 && y <= 360))
{
type = 1;
}
else
{
type = 2;
}
// 根据不同情况着色
switch(type)
{
case 0:
SetPixel(hdc,x,y,RGB(0,255,0));
break;
case 1:
SetPixel(hdc,x,y,RGB(0,0,255));
break;
case 2:
SetPixel(hdc,x,y,RGB(255,0,0));
break;
default:
SetPixel(hdc,x,y,RGB(255,0,0));
}
}
~~~
接着在相应WM_PAINT消息的时候调用LineDDA函数。
~~~
LineDDA(420,130,800,470,LineDDAProc, (LPARAM)&ps.hdc);
~~~
结果你会看到,画出来的线段是有三种颜色的。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d0beb8f.PNG)
完整的代码如下:
~~~
#include
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
VOID CALLBACK LineDDAProc(int x, int y, LPARAM lpData);
int WINAPI WinMain(HINSTANCE hTheApp,
HINSTANCE hPrevApp,
LPSTR cmdline,
int nShow)
{
WNDCLASS wc = { };
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.hInstance = hTheApp;
wc.lpfnWndProc = WindowProc;
wc.lpszClassName = L"MyApp";
wc.style = CS_HREDRAW | CS_VREDRAW;
RegisterClass(&wc);
HWND hwnd = CreateWindow(L"MyApp",
L"我的应用程序",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
35,
28,
600,
500,
NULL,
NULL,
hTheApp,
NULL);
if(hwnd == NULL)
return -1;
// 消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
switch(msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
BeginPaint(hwnd, &ps);
TEXTMETRIC tm;
// 取得与文本相关的数据
GetTextMetrics(ps.hdc, &tm);
RECT rect;
rect.top = 0L;
rect.left = ps.rcPaint.left;
rect.right = ps.rcPaint.right;
rect.bottom = rect.top + tm.tmHeight;
// 第一行文本
SetTextColor(ps.hdc, RGB(0,150,255));
DrawText(ps.hdc,L"床前明月光", -1, &rect, DT_CENTER);
// 第二行文本
rect.top += tm.tmHeight;
rect.bottom += tm.tmHeight;
SetTextColor(ps.hdc, RGB(220, 12, 50));
DrawText(ps.hdc, L"疑是地上霜", -1, &rect, DT_LEFT);
// 第三行文本
rect.top += tm.tmHeight;
rect.bottom += tm.tmHeight;
SetTextColor(ps.hdc, RGB(30,255,7));
DrawText(ps.hdc, L"举头望明月", -1, &rect, DT_RIGHT);
// 第四行文本
rect.top += tm.tmHeight;
rect.bottom += tm.tmHeight;
SetTextColor(ps.hdc, RGB(0,40,210));
DrawText(ps.hdc, L"低头思故乡", -1, &rect, DT_RIGHT);
// 绘制弧线
HPEN pen = CreatePen(PS_SOLID, 3, RGB(200, 100, 20));//创建笔
// 将笔选到DC中
auto oldObj = SelectObject(ps.hdc, pen);
// 画弧线
/*Arc(ps.hdc, 20, 100, 300, 300, 39, 110, 280, 285);
Arc(ps.hdc, 200, 160, 390, 400, 300,350, 380,165);*/
/*
AD_COUNTERCLOCKWISE表示逆时针方向
AD_CLOCKWISE表示顺时针方向
*/
SetArcDirection(ps.hdc, AD_CLOCKWISE);
Arc(ps.hdc, 20,150, 300,450, 90,162, 85,300);
// 画完之后,把原先的笔选回去
SelectObject(ps.hdc, oldObj);
// 清理
DeleteObject(pen);
// 分段线条
LineDDA(420,130,800,470,LineDDAProc, (LPARAM)&ps.hdc);
EndPaint(hwnd, &ps);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
VOID CALLBACK LineDDAProc(int x, int y, LPARAM lpData)
{
// 从参数中取得HDC
HDC hdc = *((HDC*)lpData);
// 不同位置的线段设置不同的颜色
int type=0;
if(x <= 510 || y <= 200)
{
type = 0;
}
else if((x > 510 && x <= 700) ||
(y > 720 && y <= 360))
{
type = 1;
}
else
{
type = 2;
}
// 根据不同情况着色
switch(type)
{
case 0:
SetPixel(hdc,x,y,RGB(0,255,0));
break;
case 1:
SetPixel(hdc,x,y,RGB(0,0,255));
break;
case 2:
SetPixel(hdc,x,y,RGB(255,0,0));
break;
default:
SetPixel(hdc,x,y,RGB(255,0,0));
}
}
~~~
';
(7):多边形窗口
最后更新于:2022-04-01 20:07:20
通常情况下,窗口都是一个矩形,不过,调用下面这个函数,可以自定义窗口的形状。
~~~
int SetWindowRgn(
__in HWND hWnd,
__in HRGN hRgn,
__in BOOL bRedraw
);
~~~
第一个参数是窗口的句柄,第二个参数也是一个句柄——HRGN,一个多边形的区域,可以用CreatePolygonRgn函数来创建。第三个参数指定函数调用成功后是否重画窗口,如果窗口还没有显示,就不必了,如果窗口已经显示,可以考虑设为TRUE。
我们创建用于显示窗口形状的区域句柄HRGN可以不显示用DeleteObject函数删除,因为MSDN上面有这么一句话:In particular, do not delete this region handle. The system deletes the region handle when it no longer needed.
OK,理论知识准备好了,下面就趁温暖打铁,实战一下吧。
接下来我们要做出这个模样的窗口。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d05e189.PNG)
由于这个形状是固定的,只需要设置一次即可,所以,我不打算在处理WM_PAINT消息的时候来设置多边形区域,改为在CreateWindow成功后就设置,设置之后再ShowWindow。
~~~
WNDCLASS wc = {};
wc.lpszClassName = L"MyApp";
wc.hbrBackground = CreateSolidBrush(RGB(254, 239, 180));
wc.lpfnWndProc = WindowProc;
wc.hInstance = hThisApp;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.hCursor = LoadCursor(hThisApp, IDC_ARROW);
RegisterClass(&wc);
HWND hwnd = CreateWindow(L"MyApp",
L"我的应用",
WS_POPUP,
300,140,400,400,
NULL,NULL,hThisApp,NULL);
if(hwnd == NULL)
return -1;
POINT pts[5] =
{
{ 200, 0},
{ 0, 160 },
{ 120, 400 },
{ 285, 400 },
{ 400, 160 }
};
HRGN rgn = CreatePolygonRgn(pts, 5, WINDING);
SetWindowRgn(hwnd, rgn, FALSE);
//DeleteObject(rgn);
ShowWindow(hwnd,nShow);
//UpdateWindow(hwnd);
~~~
为了去掉标题栏和边框,我在CreateWindow的时候,使用WS_POPUP。
不过,这窗口好像有点单调,于是,我想着能画些什么东西在上面,故也处理了WM_PAINT消息。
~~~
case WM_PAINT:
{
PAINTSTRUCT ps;
BeginPaint(hwnd, &ps);
//创建红色的pen
HPEN pen= CreatePen(PS_DASHDOTDOT, 1, RGB(255,0,0));
//把pen选择到设备上下文
auto orgObj = SelectObject(ps.hdc, pen);
//第一部分
POINT lnpts1[5] =
{
{200,50}, {180,360}, {220, 360}, {60,120}, {340,120}
};
BYTE bts1[5] = {PT_MOVETO, PT_LINETO, PT_LINETO, PT_MOVETO, PT_LINETO };
PolyDraw(ps.hdc,lnpts1,bts1,5);
// 第二部分
POINT lnpts2[2] = { {200,50}, {220,360} };
BYTE bts2[2] = { PT_MOVETO, PT_LINETO};
PolyDraw(ps.hdc, lnpts2, bts2, 2);
// 第三部分
POINT lnpts3[2] = {{180,360}, {60,120}};
BYTE bts3[2] = {PT_MOVETO, PT_LINETO};
PolyDraw(ps.hdc, lnpts3, bts3, 2);
// 第四部分
POINT lnpts4[2] = {{220,360}, {340,120}};
BYTE bts4[2] = {PT_MOVETO, PT_LINETO};
PolyDraw(ps.hdc, lnpts4, bts4, 2);
SelectObject(ps.hdc, orgObj);
// 删除HPEN
DeleteObject(pen);
EndPaint(hwnd, &ps);
}
return 0;
~~~
画些线条上面,让窗口可视区域看上去不那么单调。
下面是完整的代码清单。
~~~
#include
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hThisApp,
HINSTANCE hPrevApp,
LPSTR cmdlint,
int nShow)
{
WNDCLASS wc = {};
wc.lpszClassName = L"MyApp";
wc.hbrBackground = CreateSolidBrush(RGB(254, 239, 180));
wc.lpfnWndProc = WindowProc;
wc.hInstance = hThisApp;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.hCursor = LoadCursor(hThisApp, IDC_ARROW);
RegisterClass(&wc);
HWND hwnd = CreateWindow(L"MyApp",
L"我的应用",
WS_POPUP,
300,140,400,400,
NULL,NULL,hThisApp,NULL);
if(hwnd == NULL)
return -1;
POINT pts[5] =
{
{ 200, 0},
{ 0, 160 },
{ 120, 400 },
{ 285, 400 },
{ 400, 160 }
};
HRGN rgn = CreatePolygonRgn(pts, 5, WINDING);
SetWindowRgn(hwnd, rgn, FALSE);
//DeleteObject(rgn);
ShowWindow(hwnd,nShow);
//UpdateWindow(hwnd);
// 消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_PAINT:
{
PAINTSTRUCT ps;
BeginPaint(hwnd, &ps);
//创建红色的pen
HPEN pen= CreatePen(PS_DASHDOTDOT, 1, RGB(255,0,0));
//把pen选择到设备上下文
auto orgObj = SelectObject(ps.hdc, pen);
//第一部分
POINT lnpts1[5] =
{
{200,50}, {180,360}, {220, 360}, {60,120}, {340,120}
};
BYTE bts1[5] = {PT_MOVETO, PT_LINETO, PT_LINETO, PT_MOVETO, PT_LINETO };
PolyDraw(ps.hdc,lnpts1,bts1,5);
// 第二部分
POINT lnpts2[2] = { {200,50}, {220,360} };
BYTE bts2[2] = { PT_MOVETO, PT_LINETO};
PolyDraw(ps.hdc, lnpts2, bts2, 2);
// 第三部分
POINT lnpts3[2] = {{180,360}, {60,120}};
BYTE bts3[2] = {PT_MOVETO, PT_LINETO};
PolyDraw(ps.hdc, lnpts3, bts3, 2);
// 第四部分
POINT lnpts4[2] = {{220,360}, {340,120}};
BYTE bts4[2] = {PT_MOVETO, PT_LINETO};
PolyDraw(ps.hdc, lnpts4, bts4, 2);
SelectObject(ps.hdc, orgObj);
// 删除HPEN
DeleteObject(pen);
EndPaint(hwnd, &ps);
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
~~~
关于绘图方面的东东,后面会专门介绍,这里重点是说一下SetWindowRgn函数。
';
(6):创建右键菜单
最后更新于:2022-04-01 20:07:17
快捷菜单,说得容易理解一点,就是右键菜单,当我们在某个区域内单击鼠标右键,会弹出一些菜单项。这种类型的菜单,是随处可见的,我们在桌面上右击一下,也会弹出一个菜单。
右键菜单的好处就是方便,它经常和我们正在操作的某个UI元素联系起来,比如我们正在使用文本框输入文本,我们在文本框中右击,就会看到可能有【复制】【清空】【全选】之类的选项,所以,右键菜单也称为“上下文菜单(Context Menu)”。
一般来说,创建并使用快捷菜单,可以按照以下步骤进行:
1、用资源编辑器创建菜单。
2、当我们在窗口上按下鼠标右键,当系统处理WM_RBUTTONUP时会向我们的应用程序发送一条WM_CONTEXTMENU消息,我们通过响应这条消息来决定是否弹出菜单。
3、计算菜单弹出的位置,一般在我们鼠标指针的右下方,该坐标是基于屏幕的,不是窗口的。
4、调用TrackPopupMenu函数显示快捷菜单。
5、因为这种菜单是不属于某个窗口的,它的内存资源不会在窗口销毁时被回收,因此,在TrackPopupMenu返回后要调用DestroyMenu来销毁菜单的资源,释放内存。
好的,基本思路有了,我们就按照这个思路来试一试,看能不能实现一个右键菜单。
首先,用资源编辑器建立一个菜单,因为我们的弹出菜单一般只显示一系列菜项,是没有菜单的头部,不像菜单栏。因此,我们把菜单做成这样:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2cfb0648.PNG)
快捷菜单只会显示我用画笔圈起来的那部分,而上面的【abc】是不显示的,所以你可以让它空着,也可以随便输入一些内容。
然后为每个菜单项设置ID就行了,资源编辑器有时候会产生一堆没有被使用的ID宏,这些我们可以手动删除,当然也可以不管它,反正不影响程序的编译,因为头文件是不参与编译的。我们编译的时候只是编译.cpp文件。
接下来就是捕捉WM_CONTEXTMENU消息。显示菜单。
~~~
case WM_CONTEXTMENU:
{
//加载菜单资源
HMENU hroot = LoadMenu((HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE), MAKEINTRESOURCE(IDR_CONTEXT));
if(hroot)
{
// 获取第一个弹出菜单
HMENU hpop = GetSubMenu(hroot,0);
// 获取鼠标右击是的坐标
int px = GET_X_LPARAM(lParam);
int py = GET_Y_LPARAM(lParam);
//显示快捷菜单
TrackPopupMenu(hpop,
TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON,
px,
py,
0,
(HWND)wParam,
NULL);
// 用完后要销毁菜单资源
DestroyMenu(hroot);
}
}
break;
~~~
首先用LoadMenu来加载资源文件中的菜单,注意,它加载的是整个菜单栏,而我们要的是图中标注的子项。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2cfc39f9.PNG)
我们这里只有一个子弹出项,所以,GetSubMenu函数获取子项时,索引应为0。
根据MSDN文档的说明,WM_CONTEXTMENU消息的wParam参数指的是弹出菜单的窗口的句柄,lParam参数的低字位是鼠标指针的水平坐标,高字位指的是垂直坐标。
但我们不用自己去转换,我们通过GET_X_LPARAM和GET_Y_LPARAM两个宏可以把lParam中的值转为坐标值,类型为int,要使用这两个宏,需要包含WindowsX.h头文件。接着调用TrackPopupMenu来显示菜单,最后销毁菜单。
函数的具体参数我不想抄MSDN了,大家可以上MSDN查查。如果你觉得英文文档看得不舒服,你不妨使一下技巧,你可以在百度百科上搜,有中文说明,还有一些VB 6 的网站也有API的中文说明,你可以参考一下。
为了使菜单点击后程序能做出反应,我们还要捕捉WM_COMMAND消息。
~~~
case WM_COMMAND:
{
switch(LOWORD(wParam))
{
case IDM_WANG:
MessageBox(hwnd,L"你选择了王维。",L"提示",MB_OK);
break;
case IDM_MENG:
MessageBox(hwnd,L"你选择了孟浩然。",L"提示",MB_OK);
break;
case IDM_LI:
MessageBox(hwnd,L"你选择了李白。",L"提示",MB_OK);
break;
}
}
return 0;
~~~
我们来运行一下,看看能不能起作用。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2cfd58e9.PNG)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2cfe8bd8.PNG)
我们感觉到,程序好像是成功了,目的也似乎达到了,但是,如果你细心研究一下,你会发现一个问题,通常我们窗口的快捷菜单都是在窗口的客户区域右击才出现,即除了标题栏和边框,但我们这个程序,你试试,在标题栏上右击,也会出现快捷菜单,而且把系统菜单也覆盖掉了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d00c4fd.PNG)
很显然,我们是不能这样做的,很不道德,很不忠不孝不仁不义。所以,我们还要考虑一下,用户鼠标右击的位置是否在我们的客户区域范围内。要判断某个点是否在一个矩形范围内,我们可以用PtInRect函数。
于是,把上面的代码改成这样:
~~~
case WM_CONTEXTMENU:
{
RECT rect;
POINT pt;
// 获取鼠标右击是的坐标
pt.x = GET_X_LPARAM(lParam);
pt.y = GET_Y_LPARAM(lParam);
//获取客户区域大小
GetClientRect((HWND)wParam, &rect);
//判断点是否位于客户区域内
if(PtInRect(&rect, pt))
{
//加载菜单资源
HMENU hroot = LoadMenu((HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE), MAKEINTRESOURCE(IDR_CONTEXT));
if(hroot)
{
// 获取第一个弹出菜单
HMENU hpop = GetSubMenu(hroot,0);
//显示快捷菜单
TrackPopupMenu(hpop,
TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON,
pt.x,
pt.y,
0,
(HWND)wParam,
NULL);
// 用完后要销毁菜单资源
DestroyMenu(hroot);
}
}
}
break;
~~~
然后再次运行,可是你会发现,靠,问题更严重了,无论我在窗口的哪个地方右击,菜单都不出来了。
代码中是用GetClientRect函数来获取窗口客户区域的矩形位置的,我们明明是在窗口中的可视区域右击了,但为什么会没有看到菜单出来呢?我们在调用PtInRect的地方下一个断点,然后调试运行,我们来比较一下,到底鼠标右击的坐标在不在客户区域的矩形内。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d0200f8.PNG)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d0321a0.png)
有一点我们要注意的,GetClientRect它计算的标准是相对于窗口的,而WM_CONTEXTMENU取出的坐标是基于屏幕的,两个参照点不同,所以在PtInRect中无法正确地比较。所以,我们需要调用ScreenToClient函数把屏幕坐标转为客户区域坐标。但是在弹出菜单的时候,因为我们要传入基于屏幕的坐标,所以,在显示菜单前要用ClientToScreen来还原坐标为相对于屏幕的点。
即:
ScreenToClient....
..........if PtInRect
...........ClientToScreen
............TrackPopupMenu
~~~
case WM_CONTEXTMENU:
{
RECT rect;
POINT pt;
// 获取鼠标右击是的坐标
pt.x = GET_X_LPARAM(lParam);
pt.y = GET_Y_LPARAM(lParam);
//获取客户区域大小
GetClientRect((HWND)wParam, &rect);
//把屏幕坐标转为客户区坐标
ScreenToClient((HWND)wParam, &pt);
//判断点是否位于客户区域内
if(PtInRect(&rect, pt))
{
//加载菜单资源
HMENU hroot = LoadMenu((HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE), MAKEINTRESOURCE(IDR_CONTEXT));
if(hroot)
{
// 获取第一个弹出菜单
HMENU hpop = GetSubMenu(hroot,0);
// 把客户区坐标还原为屏幕坐标
ClientToScreen((HWND)wParam, &pt);
//显示快捷菜单
TrackPopupMenu(hpop,
TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON,
pt.x,
pt.y,
0,
(HWND)wParam,
NULL);
// 用完后要销毁菜单资源
DestroyMenu(hroot);
}
}
}
~~~
这样一来,就把坐标问题解决了,现在可以弹出菜单了。但还有一个问题没有解决,你会发现,现在在窗口的标题栏上右击,快捷菜单不会再出现了,但是,同时,系统菜单也没有出现。因为系统菜单是由系统来处理的,所以,解决这问题很简单,只要我们把WM_CONTEXT消息发回给系统来处理就行了。
方法一:我们判断了如果右击点在窗口的客户区域时显示菜单,那么,如果不在这个区域内,就把消息再传回给系统处理。
~~~
else
{
return DefWindowProc(hwnd, msg, wParam, lParam);
}
~~~
方法二:在WindowsProc函数的最后,统一把所有消息都返回给操作系统处理。
~~~
default:
// 如果不处理消息,交回系统处理
return DefWindowProc(hwnd, msg, wParam, lParam);
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
~~~
反正目的只有一个,把WM_CONTEXTMENU消息路由回给系统处理就行了。现在再运行一下,系统菜单可以显示。从这一点我们可以学到一个技巧,如果你想屏蔽窗口的系统菜单,你应该知道怎么做了,就是不让系统有机会响应WM_CONTEXTMENU消息就行了。另外,**按Shift + F10快捷键也会收到WM_CONTEXTMENU消息**。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-14_575fd2d0462d7.PNG)
完整的代码清单如下:
~~~
#include
#include "resource.h"
#include
LRESULT CALLBACK MyMainWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(
HINSTANCE hThisApp,
HINSTANCE hPrevApp,
LPSTR cmdLine,
int nShow)
{
WNDCLASS wc = { };
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpszClassName = L"MyApp";
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.hInstance = hThisApp;
wc.lpfnWndProc = (WNDPROC)MyMainWindowProc;
//注册窗口类
RegisterClass(&wc);
//创建窗口
HWND hwnd = CreateWindow(
L"MyApp",
L"我的超级应用",
WS_OVERLAPPEDWINDOW,
60,
25,
420,
300,
NULL,
NULL,
hThisApp,
NULL);
if(hwnd == NULL)
return 0;
// 显示窗口
ShowWindow(hwnd, nShow);
//消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
LRESULT CALLBACK MyMainWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_COMMAND:
{
switch(LOWORD(wParam))
{
case IDM_WANG:
MessageBox(hwnd,L"你选择了王维。",L"提示",MB_OK);
break;
case IDM_MENG:
MessageBox(hwnd,L"你选择了孟浩然。",L"提示",MB_OK);
break;
case IDM_LI:
MessageBox(hwnd,L"你选择了李白。",L"提示",MB_OK);
break;
}
}
return 0;
case WM_CONTEXTMENU:
{
RECT rect;
POINT pt;
// 获取鼠标右击是的坐标
pt.x = GET_X_LPARAM(lParam);
pt.y = GET_Y_LPARAM(lParam);
//获取客户区域大小
GetClientRect((HWND)wParam, &rect);
//把屏幕坐标转为客户区坐标
ScreenToClient((HWND)wParam, &pt);
//判断点是否位于客户区域内
if(PtInRect(&rect, pt))
{
//加载菜单资源
HMENU hroot = LoadMenu((HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE), MAKEINTRESOURCE(IDR_CONTEXT));
if(hroot)
{
// 获取第一个弹出菜单
HMENU hpop = GetSubMenu(hroot,0);
// 把客户区坐标还原为屏幕坐标
ClientToScreen((HWND)wParam, &pt);
//显示快捷菜单
TrackPopupMenu(hpop,
TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON,
pt.x,
pt.y,
0,
(HWND)wParam,
NULL);
// 用完后要销毁菜单资源
DestroyMenu(hroot);
}
}
else
{
return DefWindowProc(hwnd, msg, wParam, lParam);
}
}
break;
default:
// 如果不处理消息,交回系统处理
return DefWindowProc(hwnd, msg, wParam, lParam);
}
}
~~~
';