
- 第七章 进程
- 什么叫进程
- 写程序,创建进程
- 仿写任务管理器
- 给ListView添加列:
- 列举系统进程
- 接着是内存和路径的获取
- 第八章 Windows进程通讯
- 通过剪切板进行进程间通讯(新建项目:接收端和发送端)
- 创建发送端:
- 再到接收端响应这一个粘贴消息
- WM_COPYDATA消息是可以跨进程之间发送的
- 在接收端响应WM_COPYDATA消息
- 内存共享
- 第九章 Windows线程
- 什么是线程
- 用CreateThread函数创建线程
- 小实验1:运行程序d窗后,在新创建线程还没有退出的情况下,我们直接关闭主程序。
- 小实验2:
- _beginthreadex函数
- 第十章 线程同步
- 信号量
- 信号量演示代码
- 事件同步
- 事件演示代码
- 互斥量
- 原子锁
- 原子锁代码演示
- 作业(用事件通知去做数据同步)
- 第十一章 线程封装
- 1. 线程同步对象封装
- 1.1 封装临界区
- 1.1.1 我们再定义两个成员函数(加锁和解锁)
- 1.1.2 利用模板技术来做一个智能锁
- 1.1.3 自动锁
- 1.1.4 自动锁使用演示
- 1.2 封装信号量
- 1.2.1 定义锁定函数和解锁函数
- 1.2.2 自动信号量演示
- 2. 线程的封装
- 定义消息执行回调接口
- 定义MessageQueue类
- 定义消息类型
- 第十二章 类型转换
- 第十三章 自制界面库之Windows窗口自绘
- 我们一个窗口有哪些属性?
- 注册窗口类
- 创建窗口
- 窗口过程
- 设置窗口属性
- 设置图标
- 窗口大小(初始和最小)
- 窗口显示区域(GDI+创建圆角窗口)
- 按下鼠标之后的拖动区域
- 接下来我们要处理哪几个消息呢?
- (1)鼠标按下
- (2)鼠标抬起
- (3)鼠标拖拽移动的话窗口也要跟着移动
- 第十四章 自制界面库之Windows窗口自绘(二)
- 窗口消息
- WM_NCCREATE
- WM_DESTROY
- WM_SIZE
- WM_ACTIVATE和WM_SETFOCUS
- WM_ENABLE
- WM_SHOWWINDOW
- WM_KEYDOWN
- 窗口自绘
- 1)窗口固定大小的话,我们直接只要一张跟窗口一样大小的背景图就可以了;
- 2)第二种(简介),在标题栏的位置,再贴一张位图,就成了一个标题栏。
- 第十五章 自制界面库之Windows界面元素
- 界面元素
- 界面元素基本的共性
- 定义界面元素属性
- 设置界面元素属性的函数
- 基类的绘制
- 绘制背景颜色
- 绘制背景图
- 绘制标题
- 画边框
- 对于我们的宿主窗口,要提供一个函数给它:
- 按钮的自绘,先写一个框框,讲一下原理
- 给界面元素基类添加回调函数
- 监听器
- 第十六章 自制界面库之自绘按钮
- 我们先来感受下这个已经写出来的按钮
- 按钮特性
- 画按钮状态图
- WM_MOUSEMOVE
- WM_LBUTTONDOWN和WM_LBUTTONUP
- 按钮不可用状态
- 宿主窗口怎么来驱动界面元素
- 那么我们宿主窗口怎么来驱动呢
- 元素绘制的驱动
- 这里有一段代码需要讲一下:
- 实现回调
- 创建关闭按钮
- 第十七章 自制界面库之自绘标签控件
- 标签控件的特性
- 测试程序:
- 设置字体及颜色
- 第十八章 自制界面库之文本输入框
- 文本输入框特性
- 绘制边框
- 创建窗口
- 重设edit自己的窗口过程
- EDIT怎么设置字体
- 画边框
- 鼠标效果
- OVER效果
- 如何换edit的背景颜色
- 第十九章 命名规则
- 匈牙利命名法
- 上堂课的Edit有个bug
第七章 进程 什么叫进程
我们要仿写一个任务管理器:
我们发现没有打开test.txt文件。
我们来调试一下,可以看到返回值是成功的,初步判断应该是命令行参数那里可能有问题,命令行参数是以空格隔开的,所以我们给命令行参数开头加个空格试一下,发现打开test.txt成功。
然后再看看为什么dwX和dwY不起作用呢?
可以看到我们在CreateWindow的时候给相应参数传入的也是CW_USEDEFAULT,但是显示仍然不是我们指定的位置(通过给STARTUPINFO的dwX和dwY这两个分量)。
用SW_HIDE隐藏窗口也不管用;
但是我们发现dwFlags的值是0,我们再来看下CreateProcess函数的说明:
窗口的位置好像有一点点变化,但是跟我们设置的值相去甚远,这两个分量好像没用;
我们再试试SW_HIDE:
发现这个设置有作用了,窗口确实隐藏了。
看百度百科该函数的参数解释,这4个分量应该是跟应用程序本身(这里是notepad.exe)有关。
我们这里用Lesson_07.exe程序做个实验:
我们发现这4个参数确确实实是跟应用程序本身的窗口创建有关,就是说你创建的这个子进程用CreateWindow创建窗口的时候,CreateWindow的x参数必须用CW_USEDEFAULT这个值:
我们用spy++看一下ListView创建出来了没有:
给ListView添加列:
我们就在窗口显示的时候把进程快照的内容添加进列表里面,我们定义一个函数:
怎么没有显示出来呢?修改下代码再试试:
还是不显示,我们下断点看一下iRet的值:
ListView_InsertItem函数返回值为-1,该函数执行失败;给mask多加几个标志看看:
我们对lv1和lv2初始化为0试试:
作业:(1)完成各进程的内存使用情况和进程映像文件的路径;
(2)实现结束进程按钮(在进程退出前不管线程自己会不会结束,你都应该检查下该进程中的线程是不是都退出了,没有退出的线程都要关闭)。
第八章 Windows进程通讯
前四种进程间通讯方式用的比较多。
通过剪切板进行进程间通讯(新建项目:接收端和发送端)新建一个标号IDC_EDIT_INPUT:
创建控件是在WM_CREATE消息里面进行:
我们还要给这个EDIT控件添加只读属性ES_READONLY:
修改一下接收端的控件ID:
因为这个CreateWindow函数要求这一个参数是一个菜单,但是在创建子控件的时候,这里传的是子控件的ID号,这是windows规定的;也就是说我们创建界面控件的话,这里传的是一个被强转为HMENU类型的控件的ID号。
给这个发送端的EDIT控件加一个边框:
再创建一个发送按钮:
响应发送按钮:
定义一个全局变量,把主窗口引申出来:
所分配的内存并不是堆内存,不需要我们自己释放、管理,是由系统管理的,系统会自动释放。
在接收端把接收端的主窗口句柄引申出来:
先判断剪切板里面的数据格式是不是文本格式,我们只接收文本消息,然后打开剪切板、获取剪切板里面的数据:
我们要在文本框里面插入字符,首先要在光标位置选择一段空的字符:
由于SendMessage是阻塞式的,该函数执行的时候会在这里一直等待返回,所以该发送端程序不会退出;
不要用PostMessage,因为COPYDATASTRUCT结构里面有指针,PostMessage不是阻塞式的,有可能发送端程序被关闭导致指针所指向的内存失效;
我们一会可以看一下发送端这里的cdata.lpData的地址,然后再看一下接收端那里获得的数据地址,看这两者是不是一样。
可以看到这两个地址根本就不一样,所以接收端的COPYDATASTRUCT结构体中的lpData指针所指向的内存是系统重新分配的,这个内存并不是映射。
内存共享不同进程之间可以分配一块内存,这一块内存就像文件一样,给它取一个名字,各进程通过这个名字可以对这一块内存进行读写;这一种方式可以进行大数据量的交互,效率比较高。
CreateFileMapping函数:
MapViewOfFile函数:
在WinMain函数开始的时候进行一个内存映射文件的初始化:
在WinMain函数结束的时候进行内存映射文件的清理:
然后我们看一下接收端怎么来写:
我们给接收端创建一个接收的按钮:
这个程序我们得通过接收端点击接收按钮的时候,才能得到发送端传给共享内存的数据;
内存共享这种方式,我们一般是通过线程来接收,等待这个共享区有数据的时候我们才去读,没有的时候,我们就等待;即接收方开一个线程,让这个线程等待一个事件,等待什么事件呢,在线程同步里面有个“事件”(EVENT),发送方往这个共享区域写数据的时候就会触发一个事件,有这个事件了接收方的线程就会去里面读,否则就一直等待这个事件,这种使用方式等学完线程和线程同步之后再讲。
第九章 Windows线程
可以看到这个仿任务管理器的程序,没有以管理员身份运行,并没有提权限,就能获取到内存使用情况。
而且windows *** 作系统的任务管理器也有的进程路径获取不到。
什么是线程由于线程们共享进程中的资源,所以产生了所谓的线程同步,有所谓的线程锁。
_beginthreadex函数的实现原理:
可能会有这种情况:我们的这个创建的线程还没有运行完,但我们的主程序已经要退出了。
我们要在主程序退出之前,要把这个线程结束了:
这个新创建的线程一直卡在MessageBox这里。
然后我们直接关闭这个主程序(点击主程序窗口右上角关闭按钮),看看会出现什么情况:
结果我们这个主程序始终没有退出,断点一直没触发,执行流程一直没过来,主程序会一直在WaitForSingleObject这一行等下去;
当点击d窗的确定按钮,WaitForSingleObject函数就返回了,开始执行CloseHandle(g_hThread1)这一句了:
但是我们把等待时间改成5秒钟:
运行程序后关闭主程序窗口的话,d窗还在,但是超过5秒钟后,因为WaitForSingleObject函数所等待的这个新创建的线程还没结束,超时了,WaitForSingleObject函数返回,然后强行结束这个新创建的线程,该d窗就退出了。
小实验2: _beginthreadex函数_beginthreadex函数是在哪个头文件当中呢,我们点击VS右上角拐弯箭头找该函数的声明头文件:
也是如前面实验的 *** 作,我们关闭主程序窗口,5秒钟后该d窗退出。
进程:在系统里面有个进程控制块(PCB),是系统进行进程调度的时候,为了进程重入而保存的一个数据结构;
线程:也有一个线程控制块。
作业:
在这个ReadShareMemory函数里面用一个死循环去读取数据。
第十章 线程同步
(11:00)临界区演示代码
这两个线程都同时访问Func_Test这一个函数,这个函数里面就相当于一段共享资源,我们在里面定义一个局部的静态变量:
当程序退出的时候,线程的释放工作先不管了,偷个懒(以演示为目的):
这只d出了一个线程的d窗,需要修改代码:
这里出来了两个线程的d窗,同一时间产生了这两个对话框,现在我们是没有做任何保护措施的;
为了实现同一时间段只有一个线程能够访问,我们要使用临界区:
在程序开始的时候需要初始化一下这个全局的临界区变量:
在程序退出之前我们要删除这个临界区变量:
用临界区保护共享资源后再运行代码可以发现只有一个d窗:
点击确定关闭这个d窗后,另外一个d窗才会出来,即只有前面一个关闭之后下一个才能出来。
信号量信号量:它是可以跨进程的。
初始的时候可以定义有几个线程可以同时拥有这个信号量,比如我们定义3个线程可以同时拥有;
拥有这个信号量的意思,就是这个线程可以执行,不拥有的线程就挂起;
假如线程1拿到了这个信号量,如果这个信号量的计数>0,那么这个线程可以执行,同时这个计数会减1,此时计数就变成2了;
接下来第2个线程拿到了这个信号量,如果信号量的计数>0,那么这个线程可以执行,同时这个计数会减1,这个时候这个计数就变成1了;
假设这两个线程都还没执行完,都还在一直执行;
然后第3个线程拿到了这个信号量,发现这个信号量的计数还是>0,那么这个线程可以执行,同时这个计数会减1,这个时候这个计数就变成0了;
如果这个时候第4个线程来了,此时计数==0,那么这个线程4就挂起了;
等一会,线程1执行完了,它释放了信号量,这个计数就会+1,计数就从0变成1了,那么线程4就可以拿到这个信号量,就可以执行了,同时这个计数变成0;
这个信号量的计数就是允许线程访问信号量的最大数目。
我们一般都设置这个初始值lInitialCount为1。
如果跨进程,为了避免信号量重名,我们一般会用创建GUID这个工具创建一个GUID,用这个GUID来做它的名字;
如果不跨进程,那么我们可以不给它取名称,直接传参NULL或者0即可。
可以看到它也只有一个d窗,关闭之后又d出第二个。
信号量可以限制有多个线程可以同时进来访问这个共享资源的。
事件同步事件:通知消息的意思,多个线程同时等待一个可以执行的事件通知,事件在同一时刻只能通知一个线程,所以它就达到了一个同步的效果。
事件演示代码我们一般手动的事件的用的比较多;
有信号线程才能运行下去。
把事件锁定,就是让事件变为无信号状态,那么其他那些等待线程等不到信号就挂起了;
把事件解锁,就使得事件变为有信号状态。
程序测试没问题。
我们再试一下自动恢复信号的方式:
自动的话就不需要ResetEvent这行代码了。
互斥量互斥量算是强化版的临界区。
这里只讲前两个自加锁函数和自减锁函数,后面的自己看百度。
这个引用计数就是记录这个类对象被创建以后,这个对象的指针被引用了多少次。
编译发现还是报错:
修改引用计数变量的类型为volatile long才可以,这个volatile long就是对变量做一个可以被多个线程引用的声明。
InterlockedIncrement和InterlockedDecrement这两个函数本身就是内部带锁的。
这种递归调用肯定会溢出(无限制的递归调用),这里只是为了说明递归调用加锁是没有用的,第二次进入该函数执行到WaitForSingleObject函数那行不会等待,而是直接往下面跑;就是说在这种递归调用的时候(都是同一个线程),第二次进入该函数,这个锁加不加都是一个效果了,WaitForSingleObject这行代码等于就是无效的了;
这种锁只会锁不同线程,同一个线程的话是不会锁的。
我们又写了一个函数Fun_Test1这样能够看的更清楚;
在Fun_Test函数里面调用了这个Fun_Test1函数,在Fun_Test1函数里面加锁跟没加是一样的,因为我们在Fun_Test函数里面已经加了互斥量的锁,然后调用了Fun_Test1函数,在Fun_Test1函数里面对于同一个互斥量锁的话, 它这个锁是无效的了,跟没加这个锁是一样的,它是不会锁住的。
进程间通信,用这个事件通知去做一个数据同步;我们接收端线程等待发送端的事件通知,收到这个事件通知后就去读共享内存,读出来后把事件再置为有信号。
用线程+事件把共享内存这种进程间通讯方式做的更完美。
把上节课讲的5种线程同步方式进行封装,方便以后我们自己拿来用。
新建一个头文件Lock.h(我们前面也说了,线程同步本质上来说就是一个锁):
今天我们用一下命名空间。
1.1 封装临界区 1.1.1 我们再定义两个成员函数(加锁和解锁) 1.1.2 利用模板技术来做一个智能锁不需要每次自己手动调用Lock()和UnLock()函数,这样更方便一点,我们利用模板技术来做一个智能锁,我们只要在开始的时候定义一下就可以了,我们可以更偷懒一点。
这里我们现在只定义了一个临界区,我们待会要这个智能锁适应我们其他的线程同步对象(互斥体、信号量、事件),所以这里就用到了模板技术。
在临界区里面这个Type就是CRITICAL_SECTION这个类型,在其他线程同步方式里面它就是一个句柄HANDLE(线程同步对象);
LockPolicy在临界区这里它就相当于我们封装的CriticalSection这个类:
我们可以看到CriticalSection这个类的构造函数是带了参数的,而我们是传了CRITICAL_SECTION类型的指针进来的:
现在我们只是把各种同步类型用模板给统一起来了,像什么互斥量、信号量、临界区,在这个模板里面可以统一调用;
接下来我们还想把Lock()加锁和UnLock()解锁这两步给省略掉。
首先定义一个智能锁类型的成员变量:
在主函数中先定义两个线程句柄和两个线程函数:
添加一个菜单项:启动线程
多线程的程序测试框架写完了,现在还没加锁,没加锁的时候运行程序会出现两个d窗:
我们接下来给它加个锁。
运行程序测试成功;
我们这个自动锁是一个局部变量,它的构造函数里面自动调用了加锁,析构函数里面自动调用了解锁,这样子我们就免除了手动调用加锁和解锁函数了,这样子我们就不需要显式去调用各个线程同步方式里面的那些函数了。
这种重用封装的概念是C++程序员必须具备的基本能力。
1.2 封装信号量因为我们这个信号量是在线程内使用的,不跨进程使用,所以不需要信号量的名字;
当然你也可以改造成跨进程的(通过OpenSemaphore函数),有两种情形:
1)你指定其中一个进程调用CreateSemaphore函数,其他进程调用OpenSemaphore函数;
2)先调用OpenSemaphore函数,如果失败,再调用CreateSemaphore函数。
信号量封装程序测试成功。
作业:事件和互斥量的封装。
2. 线程的封装我们一般还要在主程序结束的时候等待线程的结束:
我们线程中如果有资源的时候,千万要慎重,不要轻易去调用TerminateThread函数终止线程,我们要保证我们的资源安全释放了。
我们来做这种线程的封装,先添加一个头文件:
定义消息执行回调接口我们经常有这种需要,windows里面的消息队列它是把所有消息自定义了一个序列化,有时候我们有这种需要,可能我们的一个线程它在不停地处理各种事件,那么这个时候我们来做一个类似于windows消息机制的一个线程封装。
我们定义的这个T就是一个消息类型。
我们前面讲过的WM_NOTIFY消息中的lParam参数,它用LPNMHDR这样一个结构体来代表所有的消息:
所以我们可以自定义一个像NMHDR类型的结构体,它可以包含事件的类型、它的参数。
在多态那里学过这种程序接口,但是我们不知道在什么情况下会使用它,这里就来看一下怎么使用抽象类。
我们再定义一个执行返回结果的枚举类型,我们每一次执行的返回结果,我们看到在消息处理函数WndProc中每一次执行都会返回一个结果:
这样我们就定义好这个回调接口了。
定义MessageQueue类首先我们这个线程有一个回调接口IMessageExecute,把这个接口声明成我们这个类的成员变量,同时我们要执行线程的话,这里面还要有一个线程的句柄:
析构函数里面我们要有一个停止线程的成员函数,并关闭线程句柄:
还要有一个启动线程函数:
我们在这里等一下看看这个线程执行完了没有,如果这个线程还在执行的话,代表这个线程已经启动了,那我们就不需要再去启动它了。
有没有这种可能,在启动线程的时候,这个线程很可能已经在执行了,但也有可能这个线程马上就会结束,这个线程执行完就会自动停止,所以我们在这里给它加把锁。
先定义一个自动锁类型:
如果线程没启动的话我们要启动一个线程,首先得定义一个线程函数(用该线程函数创建线程的时候要记得传参this指针):
再修改一下启动线程函数:
我们定义的这个T就是一个消息类型,类似于WM_NOTIFY消息中lParam参数,它其实是一个NMHDR结构体数据类型的指针,我们来定义一个消息类型,我们使用消息类型来决定CMessageQueue这一个不同的线程。
我们定义一个停止线程的标志m_bStop用来指示线程什么时候退出:
对于m_cMsg这个消息队列,我们还要有消息的插入函数和d出函数:
这个bActive是什么意思呢,就是说我们一开始没有消息的时候,我们的线程是不开启的;如果有消息进来了,它这个线程如果没开启的话就把它开启起来,就类似于MFC一样的那种win32消息机制。
我们接下来看一下怎么从消息队列里面拿消息?
如果执行的回调函数要求你这个线程结束的话(E_MEResult_EXIT),我们消息队列里面可能还有消息没有执行完,这些消息里面可能会有指针之类的资源,那么我们要把这些资源释放了。
我们把所有消息d出来,把它们释放了,让外面去释放(E_MECmd_RELEASE),我们封装这个的线程类里面只保存数据,至于里面资源的释放我们是不管的。
这个CMessageQueue.exute()函数就是一个消息泵。
我们来写一个类,在我们线程里面来集中处理这种菜单命令消息。
上图中这个T类型是一个类似于WM_NOTIFY消息中的lParam参数,它是一个NMHDR结构体数据结构类型的指针,这个数据结构里面可能会有一些指针的数据在里面,我们自己这个CMessageQueue线程是没法释放的,这个线程我们不知道里面有指针,那么我们要通知出去给外面一个释放的机会。
上面的是正常执行这个消息,下面这一个我们是告诉外面这一个消息要释放了,这个消息还没有执行,但是这个线程要退出了,它传过来的结构体类型里面有指针之类的,丢消息进来的人是肯定会知道传过来的消息的参数里面保存了什么类型的数据在里面,所以我们给外面一个机会,如果消息里面有指针的话就让外面的人给释放了,要不然会产生内存泄漏。
所以说我们这个消息队列里面,这个消息的参数里面可能有指针之类的数据,如果这个线程要退出了,这个消息还没有执行的话,这个消息里面的指针肯定要由谁去释放呢,要调用它的回调接口,给外面一个去释放的机会。
我们要在CWindowProc类这里面包含一个线程在里面,然后要实现从IMessageExecute接口里面继承过来的Excute函数:
我们把回调接口传给CMessageQueue:
编译类模板成员函数的时候出错,这是怎么回事呢?
是定义类型的时候用错了关键字typename,定义类型应该用typedef的。
我们要在CWindowProc类里面实现丢消息进去的函数:
这样子的话我们这个线程它就会执行起来了,我们在外面只要往这个消息队列里面扔消息的话,就会自动启动这个线程。
我们先把之前写的主程序结束前等待线程执行完毕的代码删除了:
还有菜单IDM_MENU_START这个开启线程的消息响应代码也删除了:
我们就拿IDM_ABOUT菜单项来做个实验:
我们自定义一个消息tEvent,然后把它丢到消息队列里面去。
接下来我们来看IDM_EXIT消息:
开启线程消息响应代码:
接着我们在CWindowProc类里面处理执行消息和释放资源的工作:
因为我们这个消息里面有指针,所以需要释放。
还需要修改下CMessageQueue.excute函数的代码:
如果我们外面停止了while(!pThis->IsStop()),消息队列里面还有一些消息可能没执行完,那么这里我们还是要调用一下release_execute()函数,这样子最安全了。
如果我们在这里给它返回了E_MEResult_EXIT一个退出的话,那么在线程函数CMessageQueue.release()里面就会执行release_execute,就要释放资源了。
如果我们在外面调用Stop()了,那么我们最后还是要判断一下有没有需要释放的资源,有的话还是要把它释放。
编译有错误,修改代码如下:
测试程序:
这是一个线程在处理我们这些消息。
我们在上图所选位置下个断点,点击d窗按钮关闭后断到该位置,单步步入:
如上图所示可以看到,我们得到了从外部传进来的消息:
然后又来了一个消息,我们又要去执行:
当消息队列里没有消息的时候,它会一直在excute.while循环里不停的执行,我们封装的这一个线程类还不是很完美:
没有消息了,它还在不停的循环,它抢占CPU,占CPU资源;
我们以前用过sleep这种,但是sleep不实时,打个比方睡个1秒钟或者500毫秒,刚好这500毫秒里面有消息的话,我们处理是不是就不及时了。
干脆把线程挂起,我们可以定义一个事件,如果有消息来了,那么while就正常循环执行,如果消息没来,我们就一直等待,把这个线程挂起。
这里我们定义两个函数,同学们自己去完善:
在CMessageQueue.excute函数中如果没消息的话,就调用WaitSignal函数:
在CMessageQueue.push函数这里面往消息队列添加消息的时候,就调用Signal()函数把这个事件置为有信号状态:
作业:
1)把事件和互斥量这两个同步对象封装好;
2)把CMessageQueue类中的WaitSignal和Signal这两个函数实现了。
第十二章 类型转换
第十三章 自制界面库之Windows窗口自绘
我们在项目目录里面新建一个目录XUISkin。
我们一个窗口有哪些属性?每个窗口在任务栏上有图标,有的窗口在任务栏上还显示有文字;
像那些创建窗口时的风格,在我们自绘皮肤库里面,窗口的windows风格都会去掉。
先新建一个头文件XUISkin.h,并再定义导出符号:
这样设置的话,你外面的程序包含这个头文件,XUISKIN_EXPORTS就变成__declspec(dllimport)了,导入用在你的宿主程序里面,你在哪里要用到这个dll,那就是在哪里导入。
我们在工程头文件目录和源文件目录下面各建一个目录Core:
这是个窗口。
我们新建一个类CXUIWnd:
在我们自绘界面里面,菜单都不需要,到时候我们自己做;
为了以后我们方便,写一个获取类名的函数:
为了以后便于我们注册自己的窗口类,我们只要重写一下GetWndClassName这个函数就可以了。
RegisterClass返回0就是注册失败了,如果函数成功,则返回值是一个ATOM,用于唯一标识正在注册的类。
创建窗口创建窗口时候的风格参数还是需要从外面传进来,所以修改Create函数:
大家想一下为什么这个风格要传进来呢?
因为我们有子窗口。
我们为了跟windows保持一致,所以返回一个窗口句柄。
最后一个参数LPARAM要传入this指针。
窗口过程我们重写一下窗口过程函数HandleMessage:
WM_NCCREATE这一个消息比WM_CREATE更早触发,待会我们可能要自己处理WM_CREATE这个消息,所以如果你在这里处理WM_CREATE消息的话,这个时候HWND窗口句柄还没有过来。
还记得CREATESTRUCT结构体里面是哪个成员保存了创建窗口的时候传入的this指针么,是lpCreateParams成员:
如果不是WM_NCCREATE消息的话,就从系统里面把this指针拿出来:
到这时我们就已经把窗口基本封装好了。
设置窗口属性任务栏标题其实就是窗口标题,在win7以前的系统中,最小化窗口的时候,这个窗口标题也会显示在任务栏上。
设置图标这个Icon是在任务栏上显示,这个函数的参数其实就是一个Icon的资源ID号。
当来了一个资源ID号,我们怎么来设置呢?
这个type类型可以是下图这几种,最后一个可以从文件加载:
最后一个参数fuLoad是加载类型,我们使用LR_DEFAULTCOLOR缺省标志,不作任何事情。
WPARAM参数设置为TRUE,意思是我们要重新设置这个图标,LPARAM参数就是一个ICON的句柄。
GetSystemMetrics函数中的参数SM_CXICON或SM_CYICON:
这个图标有好多种,我们随便建一个win32项目,看一下资源视图里面的ICON:
上图这里只有一种,以前的vs当中有很多种,88,1616,32*32的等等很多种,有小图标,缺省大小的图标等;SM_CXICON这种是缺省大小的。
看上图,这个时候图标就有很多种了。
窗口大小(初始和最小)拿QQ举例,刚登录打开的时候,QQ有一个默认大小,我们点击窗口边框右下角,拖动边框改变QQ窗口的大小,有一个最小的窗口的限制,不能再小了,你再怎么拖动边框也拖不上去了,它有一个窗口最小的大小。
这个窗口的初始大小我们在创建窗口的时候已经设置过了:
这个设置好了,我们接下来看一下这个窗口最小大小怎么设置,在消息处理函数中处理:
我们再定义一个消息函数:
当改变窗口尺寸的时候,我们要限制它一下,如果它改变的大小比我们设置的最小还要小的话,我们就不能让它改了。
这个WM_SIZE消息是窗口大小产生变化后的一个通知消息,并不是在这里限制窗口的大小。
窗口显示区域(GDI+创建圆角窗口)// XUIWnd.cpp
初始化一下m_hRgn:
如果没有设置m_hRgn的话,我们就没必要做::SetWindowRgn(m_hWnd, m_hRgn, TRUE);这一步了。
我们这个设置窗口区域CXUIWnd::SetWindowRgn,是要在Create函数的前面去执行,要不然我们窗口创建完了,那么这个设置就没用了;
在CXUIWnd类声明那里调整一下位置,这样符合逻辑:
在窗口创建之前你要设置好这个窗口区域,在这里我们先这样处理。
按下鼠标之后的拖动区域 接下来我们要处理哪几个消息呢? (1)鼠标按下// XUIWnd.h
鼠标一抬起之后,就要退出拖动状态,释放鼠标了:
我们之前讲MFC的时候说过,消息映射要调用父窗口的消息函数,这就是为什么我们要调用父窗口的消息函数;
父窗口消息函数为我们处理了一些功能相同的窗口事件,因为像拖拽窗口这种功能是所有窗口都一样的。
要记录鼠标按下的坐标点:
我们要获取鼠标按下的这个初始的坐标点,然后我们移动到下一个坐标点的时候,跟前一个坐标点要去比较;
计算移动的一个距离,然后与获取的当前窗口坐标(未移动前)相加得到窗口新的坐标点:
作业:
第十四章 自制界面库之Windows窗口自绘(二)
上节课的程序有个BUG:
鼠标移动的时候没有记录上一次的坐标点,所以鼠标一点之后窗口就不见了。
这个Demo是一个测试程序,
我们鼠标按住上图红色方块所在那行的话,是可以拖动窗口的;但是按住红色方块下方区域是不能拖动的。
如果我们把Create的风格参数从WS_OVERLAPPEDWINDOW改为WS_POPUP的话,窗口就变成了:
即top顶端0到bottom底端100这个范围内可以拖动窗口。
窗口消息这些是窗口自绘常用的消息。
WM_NCCREATE是窗口创建出来之后的第一个消息(窗口创建成功之后只产生一次这个消息),它告诉你窗口创建成功了,它是窗口创建成功之后发送的第一个消息,这个是窗口创建的时候系统自动产生的;
WM_CREATE:窗口创建之后的第二个消息(窗口创建成功之后只产生一次这个消息),我们一般在这里面进行窗口界面元素的初始化(子窗口的创建、窗口控件的创建);
windows的控件都是窗口,它们也会产生跟窗口一样的消息(WM_NCCREATE、WM_CREATE);
窗口销毁消息;
我们要把WM_DESTROY这个事件映射出来的原因是,方便我们在窗口销毁之后进行一些资源的释放;另外像我们主窗口销毁的话,我们应该退出应用程序。
退出应用程序,我们主窗口就销毁了。
WM_SIZE窗口大小产生变化后的一个通知消息,它与窗口实际大小的变化没关系;
我们在WM_CREATE消息那里创建子窗口或者窗口控件的时候,控件是不是有在窗口内的坐标,我们在这个消息里面调整控件的坐标,来适应窗口大小的变化,进行控件的重新布局来适应这个窗口;
我们在从CXUIWnd类继承的CMainFrame主窗口类中创建了一个CXUIWnd类型的子窗口,上图的OnCreate消息处理函数这里创建了一个子窗口(用spy++查看可以验证):
我们改下这个子窗口,让它自动随着主窗口的大小变化而变化:
// MainFrame.cpp:
我们来给XUIWnd.h增加一个函数:
编译运行程序,我们拖动边框改变窗口大小,可以看到我们这个子窗口与主窗口始终相差10个单位,这个函数OnSize主要做这个事情的(重新布局)。
WM_ACTIVATE和WM_SETFOCUSWM_ACTIVATE:窗口在激活(当前窗口)与非激活之间。
WM_SETFOCUS:这个是获取键盘输入焦点;焦点可能在当前窗口的任何控件上面。
WM_ACTIVATE和WM_SETFOCUS还是有区别的,焦点不一定在激活的窗口上面。
我们在窗口自绘的时候会用到WM_ACTIVATE,因为我们在窗口一激活的时候,我们要处理这个焦点的问题,到时候可能要在WM_ACTIVATE这里面做一些事情。
这个就先打个桩。
WM_KILLFOCUS:窗口失去焦点。
WM_SETFOCUS:窗口获取到焦点。
窗口被禁止或允许接收鼠标、键盘消息。
我们应该用过这个函数EnableWindow,用这个函数的话肯定会产生WM_ENABLE这个消息,灰色按钮克星!就是窗口不可使用的意思,屏蔽窗口的意思;
它跟失去焦点没关系,像控件你用EnableWindow屏蔽的话,它都接收不到焦点的。
WM_ENABLE这个消息我们自绘用不到,这里就不映射了。
WM_PAINT:窗口重绘;我们要在这里绘制窗口的一些背景,我们自绘主要就是在这个消息里面。
WM_CLOSE:窗口关闭消息;它和WM_DESTROY消息有什么区别呢?这个时候窗口虽然是关闭不可见了,但是这个窗口还没有销毁;WM_DESTROY是窗口已经销毁了。
先WM_CLOSE,再是WM_DESTROY;即调用Destroy函数发送WM_DESTROY消息,然后调用PostQuitMessage发送WM_QUIT消息。
WM_SHOWWINDOW就是窗口显示/隐藏消息,窗口显示的时候产生的消息(窗口隐藏也会产生这个消息),就是我们一调用ShowWindow这个函数的时候会产生的一个消息。
函数ShowWindow(HWND hWnd, int nCmdShow)的第二个参数(显示方式):
我们来试验下最小化会不会产生这个WM_SHOWWINDOW消息。
我们运行程序,然后在OnShowWindow函数里面断个点,最小化XUISkin窗口看会不会断到:
经测试不会断到该函数,也就是最小化窗口不会产生WM_SHOWWINDOW消息。
我们试试OnSize函数,运行程序,看看最小化窗口会不会产生WM_SIZE消息:
可以看到最小化窗口会产生WM_SIZE消息。
还可以通过这种实验方式试试WM_ACTIVATE消息和其他消息。
WM_KEYDOWN键盘某个键按下了;并不是键盘上任何一个键按下都会产生WM_KEYDOWN这个消息,有一部分键不会产生这个消息,像alt键、Print Screen截屏键不会产生;我们可以用程序来试一下哪些键可以产生该消息。
wParam参数里面保存了你按下键的虚拟键码。
经测试,F1、F2这些键会产生。
窗口自绘自己做皮肤库、界面库的话,我们怎么来自绘这个窗口呢?
一般我们自绘的话,会把窗口的风格改为没有标题栏,没有边框的窗口:
我们先把原来写的程序中的子窗口去掉:
我们要在上图这个窗口上自己来绘制标题栏,怎么来绘呢?
有两种情况:
先给这个窗口来一张背景位图:
其实我们可以把std::basic_string用#define定义一个别名(或者用typedef定义也可以):
我们在OnPaint函数这里进行绘制:
我们要把gdi+加载起来:
记得Release和Debug两个配置都要导入该gdiplus.lib库。
还要把stdafx.h里面的WIN32_LEAN_AND_MEAN宏注释掉:
我们在宿主程序Demo里面调用SetBground函数设置背景图片,该图片我们就用D盘下面的main.png:
此时背景图片还没有绘制出来,我们给gdi+环境初始化一下,我们先导出一个类给Demo宿主程序用:
为了简单起见,这个类我们暂时先不添加了:
我们下一次再添加,把gdi+的初始化全部封装进去,因为现在还有很多地方没有完善。
我们暂时先在宿主程序里面初始化:
在程序结束前把Gdiplus关闭:
运行测试发现还没有绘制我们的背景图片。
我们在WM_PAINT消息处理函数OnPaint里面自己不绘了, 直接调用父类的OnPaint函数:
绘制成功。
上图的图片比例跟窗口大小一样,所以适合这种拉伸。
我们再换一个背景图片:
注意看这个图片右边的边框,还是达不到我们想要的效果,因为这种图片跟窗口不同样大小,它的比例你要做的很好才行;
像这种背景图我们要做比例比较好,要刚好适合这种拉伸,由于该图比例严重失调,所以拉伸出来的效果肯定很差了;
这是第一种方式。
我们可以看到这个背景是一整张图。
2)第二种(简介),在标题栏的位置,再贴一张位图,就成了一个标题栏。如果我们想要上图的效果(背景分两部分,最上面蓝色那一条为第一部分),一般是两张图:
我们一整张背景图,然后一个标题栏图。
界面元素(简介):
我们以后做出来的界面只会有一个窗口,这个窗口上面的所有元素都不是窗口了,都是图片了,我们也就是利用WM_SIZE、WM_SETFOCUS、WM_PAINT、WM_SHOWWINDOW、WM_KEYDOWN等消息来自己做一个按钮,在上面贴位图,根据用户行为的变化,我们自己来在各个位置上面贴位图。
今天就讲到这里。
你会贴图了,你就可以把窗口风格设置成无标题栏、没边框的(WS_POPUP),然后我们就在它这个背景上面自己去绘,拿位图去贴,基本上大部分都是用位图在背景上面去贴的。
还有一部分人用MFC自己去处理各种MFC的消息,但是这种我们不提倡;一般你要做高效的话,都是用win32的自己来贴窗口的各个部分,包括它的标题栏、它的背景、按钮等。
作业:找一副位图,贴窗口背景;4个角都要圆角。
第十五章 自制界面库之Windows界面元素
有个同学问,我的窗口创建成功了,也调用了ShowWindow、UpdateWindow,但是窗口怎么不出来呢?
结果发现是在窗口过程函数HandleMessage最后的DefWindowProc默认窗口函数没有调用,他直接放回了一个S_OK;
我们的窗口过程里面,Windows还有很多其他消息,我们不需要每一个消息都自己处理,所以要调用Windows默认的消息处理函数,而上节课那些比较重要的消息我们自己处理了,我们也应该再调用默认窗口函数DefWindowProc,因为默认窗口函数里面帮我们处理了窗口创建等一些 *** 作在里面,所以不要在最后把默认窗口函数DefWindowProc给忘了;这是容易忽略的一个问题。
这个作业完成的还是不错的;
但是这个作业有一步没做,我们鼠标右击任务栏该程序点关闭的时候,这个窗口关闭了,但是这个程序没有退出;我们在主窗口里面有两个消息没处理:
什么是界面元素?
按钮、控件、编辑框等,也就是工具箱中的那些控件,我们都把它们叫界面元素;
界面元素,就是说在窗口界面上可以呈现的东西,我们都把它叫界面元素;
这些界面元素依托于窗口。
界面元素只能放在客户区吧?
不一定,比如上图标题栏上的那个叉按钮,这个按钮也是个界面元素,它就不在客户区。
spy++可能检测到的东西就是界面元素么?
不一定,像系统自带的菜单,你用spy++是检测不到的,系统自带的界面元素大部分基本上都能用spy++检测得到,windows自带的控件也都是窗口,只有窗口用spy++才能检测得到。
在我们自制界面库里面,会颠倒所有的系统自带的界面元素,我们自绘的很多界面元素spy++是检测不到的。
今天的任务:
1)抽象出一个界面元素基类;
2)自绘一个按钮。
我们知道上图界面上的控件都有一些共性,系统自带的这些界面元素至少都是窗口,都有哪些共性呢?
界面元素基本的共性1)控件上面的caption标题;
2)坐标(都在窗口的哪个位置,RECT就包括了尺寸大小);
3)ID号;
4)可见性(它是否可见);
5)是否禁用(EnableWindow);
6)焦点(Tab键);
7)背景颜色;
8)tip功能(鼠标移动上去后显示的一个提示小窗口);
9)是否有边框;
10)背景图;
我们在XUISkin工程底下的头文件和源文件各建一个目录Control:
在头文件目录中添加头文件UIElement.h:
再在源文件目录下添加UIElement.cpp文件:
焦点的话我们以后再做:
边框的话,有一个边框大小,还有一个边框颜色:
然后还要有背景图(这里是基本的,背景图要两张的那种是特化的):
设置标题和获取标题的函数:
设置坐标和获取坐标的函数:
设置ID和获取ID的函数:
设置其他属性的函数:
基本属性和属性函数都写好了,接下来就要做一个基类的绘制。
基类的绘制如果我们这个界面元素不可见,还要绘制么?所以需要判断。
我们先加一个成员函数,来判断它是否可见:
有两种不可见的情况:
1)如果直接不可见的话,就把它隐藏了;
2)还有一种不可见,我们一个窗口它是有大小的,但是这上面的控件(比如按钮)的位置有可能超出窗口区域,即元素不在窗口可见范围内,这样的话就不用绘制了。
宿主窗口相当于父窗口,但它并不是父窗口,因为我们绘制的界面元素它们是没有窗口句柄,所以我们有一种专业的叫法,叫宿主窗口,就是这个界面元素依托于哪个窗口。
我们的窗口跟宿主窗口的客户区的可见区域如果没有交集,我们的窗口不在宿主窗口的客户区域的话,它们的交集是空的矩形,那么就是不可见。
如果只超出了一部分,那么没超出的部分我们肯定也要绘制。
3)其实我们还要判断一种情况:
我们窗口的width=0或者height=0,也是不可见,也不需要绘制。
这个元素尺寸可以调到前面,放到交集前面:
我们有几个与窗口绘制有关的属性(标题、背景颜色、边框、背景图):
上图所选的这几个需要绘制,那么我们应该按照什么顺序来绘制呢?
为了避免被覆盖掉,我们的绘制顺序应该为:
1、绘制背景颜色
2、绘制背景图
3、绘制文本
4、绘制边框
我们有些界面元素,像按钮会有鼠标状态,我们可以在放完背景图之后,加一个函数用来画状态图:
但是这里的这个画状态图的函数我们做成一个虚函数,因为不是所有的界面元素都需要画这个状态图,所以这里面什么都做,到时候在写子类的时候,你要画状态图的话那么你自己画去,我们这里只给出一个“通道”,到时候你只要重载这一个虚函数就可以,你重载了这个虚函数,你就有了绘状态图的功能;如果你不需要这个功能,那么你就不重载,那么这个函数什么都不做。
这个状态你想绘什么就绘什么。
这种直接绘背景颜色的,我们就不用gdi+画了,直接用gdi来画了:
我们这个背景颜色先给它设置一个默认值:
画这个背景图用到了昨天讲过的gdi+来画的:
我们用这个现成的绘制来画背景图:
提问:这里传graphic会不会比hdc要好?
说的对,这里确确实实传graphic要好一些,让宿主决定graphic的生命周期比较好,我们修改代码:
RGB:高8位为R,中间8位为G,后8位为B;
ARGB:高8位为A,次8位为R,再次8位为G,最后8位为B。
我们这个标题需要有文字的颜色和字体:
绘制文字:
字体:
还需要定义一个字体大小:
看一下Gdiplus::PointF的构造函数是什么样子:
这样子,我们的抽象基类就写完了。
按钮的自绘,先写一个框框,讲一下原理按钮好做,而edit控件不好做,edit控件我们用到的时候就用现成的,edit控件要自己做的话就太麻烦太多内容了。
我们再来给它添加一个cpp文件:
我们的界面元素基类里面还少了一个回调函数:
我们的消息来了的话,我们要让控件处理与自己相关的系统消息。
返回值如果是S_OK的话,就是子控件成功处理了这个消息,宿主窗口无需再处理;
返回值如果是S_FALSE的话,就是此消息与子控件无关,要继续由宿主窗口处理;这个FALSE就是失败,失败了的话就是子窗口处理失败,由宿主窗口处理。
默认是返回S_FALSE。
监听器像有些控件的鼠标点击事件,我们还要加一个回调函数,告诉父窗口我这个按钮被点击了的回调,一个事件通知,就是我这一个按钮发生了什么事,我要告诉父窗口。
打个比方,像我们一个按钮,在这个按钮上面点击了鼠标的时候,消息就会到OnControlMessage函数这里面来,那么我这里面就处理了,我鼠标按下的时候,我首先肯定要自绘,要把我控件被鼠标按下的状态改变,同时也要通知父窗口我这个状态改变了,我这个按钮响应了一个鼠标按下的事情,那么我们就执行一个相应的按钮按下的一个 *** 作。
上图所选的本来应该是一个事件类型,这里还没想好该怎么做;我们先做一种,就是一个鼠标点击;将来我们再拓展。
这就是弄了个监听器。
OnControlMessage函数少了一个参数,我们修改之:
我们首先要判断一下当前鼠标点击的光标是不是在我们这里面,即处理鼠标按下事件:
我们可以通知父窗口鼠标按了哪一个控件,所以我们还要加一个ID号,修改原代码中的UINT类型的m_uID为StdString类型的m_strID:
我们通知这一个回调,通知我的宿主窗口我被按下了,被按下了的话,我这个按钮就可以响应按钮的鼠标点击事件了,这就可以通知了。
这就是23种设计模式中的监听器模式。
我们加一个创建界面元素的Create函数(绑定监听器):
自绘按钮剩余的内容我们下节课再讲,这里先把基类设置好了。
作业:我们试着根据基类框架CUIElement的认识,自己写一下这个按钮的自绘,在主窗口CMainFrame类里面做按钮按下事件的监听:
这里可以判断它按下了哪个按钮。
第十六章 自制界面库之自绘按钮
有时候在编译的时候,发现导出的API或者类,链接出错,这该怎么解决呢?
在解决方案里有一个编译顺序,像上图这个Demo是依赖于XUISkin的,像我们这个皮肤库里面导出的接口改变了,那么你在编译的时候肯定是先编译XUISkin,但是我们可以在解决方案里面设置这个编译顺序:
看上面我这里设置的,是先编译得皮肤库XUISkin,再编译的Demo,怎么设置呢?
如上图设置,我这个Demo依赖于皮肤库,打个勾的话,那么编译的时候就先编译XUISkin,然后再编译Demo这一个。
如果你没设置这个生成顺序的话, 可能要经过多次编译(多次按F7生成解决方案)才能够编译过去。
先把按钮上的文字去掉,我们先不绘文字:
把上图所选这行删除掉:
我们先来感受下自己绘制的这个按钮,鼠标移上去按钮是一个效果,点击不松的时候又是一种效果,它都有不同的效果;
现在点击按钮没反应,我们修改代码的注释符如下:
此时点击按钮后会d窗。
按钮特性1)响应鼠标点击事件
2)tip
3)按钮有几种状态
3.1)正常状态(没有鼠标事件的时候)
3.2)鼠标划过(over)的效果
3.3)鼠标按下
3.4)获取到焦点
3.5)禁用
根据鼠标在按钮上的情况,按钮可能有几种状态,例如按下后像陷下去了一样,鼠标放在按钮上面会发亮,鼠标移上去按钮会有效果。
我们这个按钮常用的几种状态:
我们刚刚看到的按钮状态就是上图这张图片,就是根据鼠标这几种状态定义的顺序,我们上面的这张图片要跟鼠标这几种状态一一对应,这张按钮状态效果图里面的图片数量可以大于等于1,小于等于5(鼠标状态的数量)。
这个m_iStateCount成员变量是说我们这个按钮有几种状态,我们切图的时候可以根据这个m_iStateCount状态的个数,可以用函数把这张图片平均几等分;
m_strStateImage这个成员变量要保存这个状态图,要不然我们到哪里去切呢,我们需要一张图片。
所以我们需要两个成员函数,一个设置状态图的,你要告诉这个按钮它有几个状态iStateCount,我们默认至少会有4个状态(1、2、3、4),我们修改下鼠标状态的顺序:
我们上节课做界面元素的时候有一个画状态图的函数DrawStatusImage:
当时我们做的是一个空函数,因为我们这个基类的元素里面没有所谓的状态,但是我们其他按钮会有状态,所以我们把它设置成什么都不做的虚拟的空函数,今天我们的按钮主要就做这个函数就可以了,我们来看一下这个状态怎么画。
画按钮状态图我们要切图,首先要把图片加载上来:
第二步,就是上图所选那行,这句是什么意思呢?
获得单个图坐标。
就是我们根据它的一个状态m_iStateCount,首先对图片进行四等分,按钮有几个状态m_iStateCount那么就把我们这个图片分成几等分,就是计算按钮的一个宽度,如上图我们有4个按钮状态,所以对上图4等分,每个图的宽度是:im.GetWidth()/m_iStateCount*1
而后面的 *(m_eState - 1))是说,我们第一张状态图(m_eState=1)它的左边就是0(1-1),第二个图片左边的坐标就是im.GetWidth()/m_iStateCount*(2-1),第三个图片就是图片的宽度*(3-1),以此类推。
第3步,就是切图,画状态图了。
这个destRc就是按钮本身在宿主窗口上的坐标,因为每个元素都有它在宿主窗口上面的坐标位置。
然后就是绘图了,第1个参数是图片,第2个参数就是我们要绘制在窗口上的哪一个位置,它是一个矩形,是按钮在窗口上的坐标,它要绘制在哪里;
第3个参数就是我们刚刚计算的左边位置,我们要从这幅图片上的哪个位置开始绘制(也就是左边它的x坐标),第4个参数0就是这幅图片左上角的顶端肯定都是0,第5个参数就是切片图片的宽度,就是取这幅图片左边iLeft指定的点开始,它要切多少出来、切哪张图出来,第6个参数就是高度;
第7个参数就是按像素点来绘制,像我们位图的话都是按像素点绘制的:
绘制完了之后,该做什么呢?
根据事件的不同,我们来设置这个按钮的状态。
我们之前定义界面元素基类CUIElement的时候,定义了一个控件的一个事件响应的虚函数:
所以我们接下来在CXUIButton重载这个事件控制的函数:
第一个就是鼠标移上去的事件,我们要判断一下鼠标点是否落在我们的按钮上面,如果它不在按钮上面我们就不需要去管它了。
这个HitTest函数是在基类CUIElement里面定义的:
为什么在基类里面把这个函数定义成虚拟的呢?
就是因为有时候我们判断的这个点不一定刚好是矩形区域,可能还要进行一些特殊处理,那么我们所有调用这个地方的都不用变,写到后面的时候毫无疑问会有多个地方调用它用来判断,根据不同界面元素的要求,不一定会用PtInRect函数去判断,不是根据m_bound它来判断(因为有些可能不是矩形区域),所以这里就把它封装了一下,这是为了将来扩展可重用。
为什么这里要判断它不等于E_BtnState_Over这个状态呢?为什么要这样写呢?
因为有可能我们已经是over状态了,就没必要重绘了,没必要再进行一个绘制 *** 作了,节省开销了。
我们在这个按钮本身内部(鼠标指针不离开按钮图片区域)移过去、移过来,是不需要再重绘的。
这种严谨的逻辑思维很重要,可能你有这种思维的话你写出来的程序确实比别人的效率高很多,所以要养成这种思维。
我们看到这个SetState只是切换了变量m_eState的数值(状态),并没有重画啊?
我们来看看SetState函数:
我们先把状态改变了,接下来要告诉我们按钮这个状态变了,然后我们在OnChangeState函数里面做了什么事情呢?
现在我们这里做的比较简单,只是把这个区域置为无效,引起重绘,而这个InvalidBounds函数我们已经封装到这个基类CUIElement里面来了:
我们把m_hHostWnd这个窗口中的m_bound这个元素区域重绘一下。
如果这个鼠标的状态不等于E_BtnState_Normal这一个标准状态的话,那上一次鼠标肯定是落在了按钮上面,如果上次鼠标没落在按钮上面,那么我们就没必要在这重绘了。
以上这些就是我们鼠标划过效果的实现,就是鼠标移上去状态效果的实现。
WM_LBUTTONDOWN和WM_LBUTTONUP接下来是鼠标按下的状态,就是鼠标左键按下的时候:
先获取鼠标的一个状态,然后又是一个判断,判断按下的时候是否在按钮的区域内(HitTest函数)。
鼠标抬起:
这是我们上节课做的回调,用来响应这个按钮的,抬起的时候响应鼠标点击事件,我们点击完了之后,恢复这个按钮正常状态。
我们还有两个按钮状态效果没实现:
这个焦点还不到讲的时候,现在先不讲。
按钮不可用状态按钮不可用状态,我们这一个还没有写任何代码,但是这个按钮不可用的这个状态的效果其实我们已经实现了,为什么说这个不可用状态的效果已经实现了呢?
E_BtnState_Disable这一个的触发,首先一点,这一个状态不是鼠标触发的状态,它是程序员根据程序逻辑自己来触发的,那么我们怎么来触发呢?
我们看CXUIButton类的SetState这一个接口:
我们看一下这个里面是怎么做的:
我们刚刚已经说过了,通过OnChangeState这一个通知重绘的时候,我们再看一下这个绘制状态的DrawStatusImage函数:
这里是绘制所有状态的,这个是通用的,它本身是根据状态来绘制的,那么按钮不可用的状态是不是就已经实现了呢,它已经实现了。
这个SetState函数是只能通过外面的程序员自己来根据程序的逻辑需要,自己来调用这个函数,其实这个也就相当于EnableWindow函数,你直接设置一个Disable状态,就相当于外面窗口里面的EnableWindow这一个函数。
这个只是实现了状态效果,但是功能还没实现,我们刚才说了设置Disable状态效果的话,那么它就不响应其他消息,它不响应事件,这个按钮不可用。
在什么情况下这个按钮本身不可用:
1)E_BtnState_Disable
2)不可见的时候
外面自绘的按钮它不是窗口,它响应事件是靠的宿主窗口来驱动的,那么就是说,尽管外面的按钮不可见,也会有一种情况,我们在HitTest函数这里判断的时候:
它也会产生一个鼠标落在这个上面的效果,程序流程也会进入到上图这里面来,也就是没有按钮也能响应事件,所以这个我们要规避,这个是不正常的现象。
所以这个HitTest函数的特殊效果就来了,我们要在CXUIButton类这里重载这个函数:
只有在可见的情况下,我们才去做这种判断(HitTest);
如果不可见,即使鼠标落在我们这个按钮区域范围内,那么我们也不会去做这种判断(HitTest)。
我们这个按钮不可见的时候,我们这个绘制CXUIButton::DrawStatusImage函数里面也已经处理了这种情况:
像我们这里连绘都不绘制,直接返回即可。
宿主窗口怎么来驱动界面元素我们已经定义了界面元素的消息函数,这一个消息函数是用来便于宿主窗口来驱动这个事件的,我们控件的事件在CUIElement::OnControlMessage这个函数里面:
而我们按钮这里刚刚重写了它:
从上面没看出来啊?
我们回到Demo程序里面的MainFrame主窗口:
因为我们还没有做界面库的框架,所以暂时我们就在这个主窗口里面实现了,虽然这样做窗口形状就定死了,我们暂时先这样写。
我们看下图CMainFrame中的HandleMessage消息处理函数:
我们在CMainFrame::HandleMessage这里面调用了控件的OnControlMessage消息函数,我们先来看一下这个函数的返回值:
看到这个返回值没有,如果与子窗口无关的话,继续由宿主窗口处理,如果子窗口处理了,那么就无需再处理。
我们驱动就在这里了,这个就是驱动它的一个消息;
我们接下来还有一个绘制没驱动。
我们在窗口里面加了一个元素绘制的虚函数:
这个函数里面什么都没做。
我们来看一下OnPain绘制里面调用了一个什么:
从上图可以看到在这里调用了刚刚讲过的元素的绘制函数:
我们这个主窗口CMainFrame从CXUIWnd继承过来之后,在这里进行了一个绘制:
因为我们还没做界面库的框架,暂时先放在这里了。
这里有一段代码需要讲一下:我们这个窗口基类CXUIWnd的绘制函数OnPain,我为什么把代码改成下面这种呢,gdi+的双缓冲绘制,防闪的,我讲一下这段代码,在gdi+里面怎么做双缓冲:
双缓冲是什么意思呢,就是先把界面所有的元素在内存里面绘制成一张图片,绘制完之后,把这个缓冲图片一次性绘制到窗口上面。
Gdiplus是一个命名空间,Graphics是一个类,FromImage是一个静态函数:
Gdiplus::Grahpics::FromImage函数是Graphics类里面的一个工具函数,根据内存位图(Bitmap)生成了一个相应大小的内存绘图器(Graphics),你可以把它看成一个画布。
在画布上画好了之后,再把画布一次性绘制到设备上:
上面代码把背景图绘制在内存绘图器上面,也就绘制到了内存位图上面了;把界面元素绘制在内存绘图器上面,也就绘制到了内存位图上面了。
这种处理就是为了防闪烁的,就是现在内存里面画好:
这个位图membmp它的内存跟这个绘图器pGrx都是同一块内存;
等于就是说,它在内存相应区域里面准备了一块内存跟我们这个窗口
最后绘制到窗口的位图和画布上的是同一张吗?
不是同一张了,这个是复制了一张,grx这个窗口DC它有自己的位图,它只是把内存DC的一个背景位图绘制到grx这个窗口DC上面。
然后再看看这个Graphics::FromImage函数,由于它是new出来的,所以最后要delete释放内存,我们看MSDN的解释:
所以修改代码:
原则上来说,dll里面new出来的指针,应该是由dll自己来管理的,最起码至少要给个删除的接口给我来释放,但是我们找遍了没有这个东西。
像这个内存绘图器pGrx不应该这样写,因为这种刷新是经常做的,不停的new不停的delete的话,我们应该把它做成一个成员,就不需要经常new出来之后马上再delete,因为重绘的发生的频率是比较高的,不适合用pGrx这种局部变量,到时候我们做界面库框架的时候把它做成一个成员。
实现回调接下来我们来看一下回调的实现,我们又要回到CMainFrame这一个窗口里面去:
本来这个IXUIEventCallBack类可以移到基类CXUIWnd里面去,这里先暂时这样,不影响我们的实验。
这个就是回调,是一个虚函数。
在这里就响应我们的按钮事件,我们来看一下这是怎么响应的,我在CMainFrame类里面声明了一个成员OK按钮m_BtnOK:
而这个界面元素子控件是在哪里创建的呢?
在OnCreate里面去创建的:
这第一个m_BtnOK.SetStateImage函数是设置图片,默认参数值是4,我们这个按钮图片里面也是4个的:
这个Create函数第一个参数是元素的ID,这里我把它改写了,第二个参数是坐标,第三个参数是宿主窗口。
像这种你必须得这样子做,因为以后我们要做的那个xml解析,我们到时候怎么解析的呢,都是一行一行的解析,一行一行的解析的话那你怎么可能一次性把它解析出来了呢,我们都是一行一行解析的,解析一行,是个什么类型的元素,然后生成一个,生成一个之后再去读它的参数,是这样子的。
我们这个创建函数虽然最后一个参数需要传一个回调函数过来,我们这里就把this传进去了:
我们这个关闭按钮是5态按钮。
这幅图片它的大小是9018,由于里面是5个图片,也就是说每一个按钮的大小是1818的,下图所选部分就是计算18*18的,放在右上角(rcWnd.right - 23):
接下来打开绘制和事件响应:
运行程序后发生崩溃,单步调试代码发现问题,把下图所选删除:
我们点击关闭按钮后,就d出一个窗口,我们需要点击关闭按钮后退出应用程序:
这样子按钮的重绘就完成了。
我们把另一个按钮的代码也打开,我们一共做了3个按钮:
今天按钮的自绘我们就学完了。
我们今天先把这种写死的先学好了之后,我们后面会学自己来做这种框架,来动态地绘制界面。
作业:
1)完善上节课的自绘按钮,必须达到这里讲的效果;
2)用这段时间学的自绘原理,自绘一个static text控件。
上完课之后,你要去总结一下这个按钮它的绘制原理,它的特性。
回顾上节课自绘按钮的关键点:
1)背景图的切图绘图;
2)控件的状态变化,怎么来控制状态;
3)事件(事件控制)。
我们先来理一下思路,你拿到这个界面元素的时候,它有几个关键点:
1)所有界面元素它都有背景(图片也是背景),首先把这个界面要画出来,绘制一个图出来,界面你要呈现出来;画出来有几种方法:
1.1)通过颜色像素点,gdi/gdi+里面的API背景填充、;
1.2)要么是通过位图来贴图。
界面元素你就要有保存颜色的成员变量,图片的话你需要有保存图片的成员变量;你要改变背景的话,就需要成员函数来改变这些变量,这些都是顺理成章的问题,你要去思考那些软件界面是怎么画出来的,基本原理都是一样的。
2)思考这种特定界面元素它的特性
这些特性来源于平时你自己使用别人软件的时候,这种界面元素会有一些什么特性,特性包括:事件引发的界面元素的变化(鼠标移上去的效果),根据我们前面学到的事件、系统消息,这种效果我要怎么来实现,通过特定的事件来怎么改变界面元素它的呈现方式;你要思考这些问题。
直接用来呈现文本,但是不可修改的,内容是不可以被复制的;这种标签是我们控件当中最简单的。
1)用来显示一段文字;
2)不可以用鼠标选择;
3)鼠标移上去字体变颜色。
我们来添加这个类,新建一个头文件:
我们再来添加一个cpp文件:
文本这个成员变量在我们基类CUIElement里面已经有了:
而文本颜色、字体、字体大小在我们基类CUIElement里面也都有了:
在基类里面这个文本绘制的区域位置也有了,在这个区域里面有一个对齐方式,左对齐、右对齐、上对齐、下对齐,也就是说我们的文本的呈现风格,是靠左对齐呢,还是靠上,靠下呢,这个文本风格是需要我们定义的:
第2个特性我们不用去管;
我们需要有一个字体高亮显示的文本颜色:
字体颜色有两个,那么我们要想个办法控制它,我们要有一个变量记录鼠标移上去了,这样我们就不用每次鼠标移上去后还再去判断鼠标在不在字体上面;
看一下我们按钮有几个状态:
但是我们静态文本框的话它就只是这一个状态,就是鼠标移上去就高亮显示,所以这里就一个变量去记录就可以了,记录是不是需要高亮显示:
我们基类里面定义了几种绘制方式:
因为我们需要绘制文本,所以就把DrawText重载了:
我们可以看到静态文本标签的背景是透明的:
gdi+里面有个DrawString函数,它有3种重载方式,我们只能用下图这一种,因为它有设置格式的参数stringFormat:
我们先来创建字体:
再创建一个画刷:
怎么设置m_bLightColor这个变量呢?
这个要根据鼠标事件来设置。
上图所选函数是响应鼠标的,那么我们要来把这一个重载一下,如果鼠标移到我们这个上面来了,那么我们就要高亮显示。
鼠标移动的时候我们要判断一下:
那么我们接下来看一下怎么生成这个静态文本框;
我们给窗口加上一个标题:
两个按钮的样子不对,我们改一下,把它们的bottom的值改为130:
但是还有一个问题,当鼠标移动到标题文字同一行上的时候(鼠标不在标题文字上),标题也会高亮显示:
这是为什么,什么原因造成的呢?
我们设置的区域比较大,而我们的文字比较少,我们要把这个问题解决一下。
我们要先重载一下HitTest函数:
我们要来计算下这个文本的长度,怎么计算呢,我们声明一个成员变量来记录这个文本区域:
然后我们在CXUIStaticText::DrawText绘制文本函数中:
我们这里的if(m_rcText.right - m_rcText.left < = 0)代表只计算一次文本区域,我们还是不加这行代码算了,因为我们文本可能会改变,在程序运行过程中这个文本是可以改变的,所以需要每次计算一下;在gdi+里面专门有一个函数Gdiplus::MeasureString计算这段文本的区域。
文本区域我们已经计算出来了,那么我们接下来就可以判断了:
现在鼠标指向标题同一行后面的部分就不高亮了。
字缝(字与字之间的空白部分)怎么判断?
字体里面我们有一个设置字间距的,
你指定一个字间距,然后有多少个字,不就可以计算出来了么,感兴趣的可以自己去计算一下。
现在我们鼠标放在标题上是可以拖动整个窗口的,我们不能让鼠标按着标题拖得动,怎么来改变呢?
鼠标左键按下在我们这个标题文本区域内的话,要把这个按下事件过滤掉,就这么简单:
OnControlMessage这个回调函数返回值的含义还记得么?
我们基类CUIElement里面没有字体及颜色的成员变量,增加相关成员,设置字体大小,设置字体及文本颜色:
然后在这里来调用这些函数设置一下字体、字体大小:
我们可以看到文本的区域太小了,显示不完整,修改代码:
改为楷体看看:
为了兼容,我们不设置LightColor高亮颜色的话,看看是什么效果:
鼠标移到标题上就不高亮显示了,说明我们设置对了,再恢复高亮显示那行代码。
这样子,这个静态文本框我们已经完成了。
咱们写的这些自绘控件肯定比微软提供的效果高,因为这些已经不是窗口了,而窗口的开销要大一些。
第十八章 自制界面库之文本输入框
文本输入框有Edit和RichEdit这两种,RichEdit属于无窗口控件, *** 作系统自带的这个RichEdit本身就有两种,一种是无窗口的,一种是有窗口的。
我们控件有无窗口控件和有窗口控件这两种之分,我们今天只讲Edit,要自己做无窗口控件RichEdit的话需要比较高深的com技术。
spy++抓不到Edit的原理:
有些Edit我们用spy++抓不到,其实不是抓不到,这个Edit失去焦点的时候,它就隐藏了而已,把Edit里面的文本画在了宿主窗口上面,鼠标点击的时候就把这个Edit显示出来。
你也可以把Edit做成不是窗口的,但是要计算光标,这个东西比较难计算,没必要这样子计算。
这个只是一种方法,输入法也是一种。
我们做的这个Edit要实现的效果:
我们今天先做简单的,这种原理学好之后,做复杂的也能做出来。
它不是重绘Edit本身的边框,重绘Edit的代价太高。
我们可以在宿主窗口上面自己绘制一个边框,然后把我们无边框的Edit放在这个位置就可以了,并响应鼠标事件。
常用的控件里面这个Edit比较特殊一点,你要画它的背景, 包括改变它的自己颜色,我们待会讲它特殊在哪里。
文本输入框特性1)它是一个窗口,有窗口的话就有窗口过程;
2)这种控件不需要我们自己注册窗口类,以前我们都是直接创建的(CreateWindow(“edit”, … 或者CreateWindow(“combox”, …),不需要我们再注册窗口类了;
3)但是这里有一个问题,不需要我们自己注册窗口类的话:
不需要我们自己注册窗口类的话,那么我们没有办法在上图注册的时候给它指定一个我们自己的窗口过程;
那么我们怎么来指定这个窗口过程呢?
4)我们要向处理事件的话,要重设Edit的窗口过程。(肯定是没办法在注册那里指定了,我们得通过其他方法重设它的窗口过程)
今天我们要实现的几个效果:
1)我们要绘制自己的边框;
2)要响应鼠标事件。
这个XUIEdit它是一个元素,同时它也是一个窗口,从窗口类也继承一下,所以要从这两个类继承。
利用CUIElement这个元素来绘制边框,CXUIWnd是一个窗口,所以也要用到。
我们看到对于这个Edit编辑框,鼠标有3种效果:
然后还要设置高亮显示的效果(鼠标移上去的效果、鼠标点击的效果):
如果这个窗口存在的话,而且m_eState不等于正常状态的话就重绘,为什么这里要加这两个判断呢?
SetLightColor这一个函数有可能在窗口没创建之前就调用,也可能之后调用,窗口不存在的话就不画。
元素类里面有一个绘制边框的虚函数,我们要把它重载了:
创建窗口现在我们画边框已经画好了,接下来该做什么呢?
我们还有一个窗口没创建,我们要重载Create函数,我们窗口类里面有一个Create函数:
我们元素类里面也有一个Create函数:
这种控件我们要写一个自己的创建函数:
其实第一个参数ID你可以填空,不需要这个ID照样可以创建出来。
这个就是我们自己写的Create函数。
它继承于这两个类,那么我们首先把这个元素的创建直接调用基类的,然后再创建窗口:
我们画边框的话就要把边框的坐标给留出来,还要再创建一个设置风格的函数:
自绘窗口都不设置标题,我们标题都是用CXUIStaticText类画出来的。
重设edit自己的窗口过程我们有一个API函数,SetWindowLongPtr,这个函数就能修改指定窗口的窗口过程:
SetWindowLongPtr这个函数我们在窗口类那里也用到过,在创建的时候传了一个this指针进去:
但是刚才也说了我们自绘的edit是一个比较特殊的窗口,它是不需要注册的,我们这个注册CXUIWnd::RegisterWndClass肯定会失败,所以edit创建的的时候肯定不会到基类的这个CXUIWnd::WndProc里面来,那么我们还要在修改窗口过程之前先把this设置进去:
发现有一个问题,在父类CXUIWnd的Create函数中,注册失败的话不是直接return了么?
所以需要修改代码,把CXUIWnd::RegisterWndClass作为一个虚函数,在CXUIEdit中重载一下它就不会失败了:
我们刚刚说了,这个edit是一个特殊的窗口,它是不需要注册的,但是我们要在这里需要做一些事情,用GetClassInfoEx函数来获取到已经注册的窗口类的信息,为了使用这个函数,还需要重载一下CXUIWnd类中你的GetWndClassName函数:
为什么要获取已经注册的窗口类信息呢?
我们要拿到EDIT控件默认的窗口过程(这个WC_EDIT窗口类是系统已经注册过的),把它保存下来:
为什么要保存这个默认的窗口过程呢?
因为EDIT、COMBOX这些控件是比较特殊的窗口,它不能用那个通用的默认窗口过程DefWindowProc,这里我们要调用EDIT控件本身默认的窗口过程m_pOldWndProc。
我们还要重载一下CXUIWnd窗口基类中的窗口消息处理函数:
在我们自己的窗口过程里面,默认的时候要调用EDIT控件本身默认的窗口过程m_pOldWndProc。
我们测试一下,背景先不画,先看一下这个edit能创建成功不能:
如果我们调用另外那个通用的默认窗口过程DefWindowProc,你看一下是什么效果:
这个edit像打了一个孔,也就是没创建出来,看到这个是不是你自己也会打孔了。。。。。。透明窗口?哈哈!
程序流程走到上图这里说明注册是成功了的,要注册失败的话窗口过程怎么会过来呢。
edit、combox、list这些控件它们的窗口过程不能调用这个默认的DefWindowProc,它们有自己的专用的默认窗口处理过程;
这就是为什么我们要在注册那里来获取edit默认的窗口过程,它跟DefWindowProc这一个是不一样的,它为edit控件做了一些特殊的处理。
先获得一个默认GUI字体,然后设置:
edit以及其他控件,都是这样子设置的。
这个字体确实变了。
这个edit我们创建出来了,但是它没有边框。
画边框首先我们要设置下边框大小、边框颜色,以及高亮颜色:
现在有了一个颜色很淡的边框了,这个颜色你自己随便设置:
现在还没有鼠标效果。
鼠标效果 OVER效果我们发现它失去焦点的时候还是有问题,我们点击edit编辑框以外的父窗口的时候,这个edit没反应;
我们还得重写一个函数,还有一个窗口过程没有重写,还有元素类的消息处理函数没有重写:
然后我们调用一下:
现在可以响应到了。
为什么要在这里加这种坐标点判断呢?
这个是元素,不是窗口,它执行的是宿主窗口,所以要判断坐标;
鼠标移动,如果移出edit,使用窗口过程,edit状态不会改变。
我们自己做的这个控件,它可以响应两种消息,一个是自己的消息,还有一个它父窗口的消息,因为我们这个是元素和窗口的混合体。
如何换edit的背景颜色这类控件是比较特殊的窗口,只能在父窗口里面改,除非你不用系统自带的edit,自己写一个。
这些都是比较特殊的窗口,它们都有专门的的消息,只能通过父窗口来改:
这个消息中的wParam保存的就是edit的HDC,我们只能在这里进行一个设置:
这样一改所有的edit背景颜色都被改了,这里只是演示一下,我们自己以后要改的话,那么我们肯定要专门做一个针对于不同的窗口,参数lParam保存的就是edit的句柄。
不能强行元素重绘么?可以试一下。
它里面是做了特殊处理的,你在这里重绘是达不到你要的效果的,虽然开始的时候你看到的edit背景是改变了,但是你一点击edit或者一拖动整个窗口,edit背景就有问题了;
所以你只能在父窗口里面去重绘,去设置它的背景颜色:
而且你在edit的消息处理函数里面设置它的透明背景是没有用的,它的默认窗口过程又处理了一些事,就是SetBkMode(hdc, TRANSPARENT);这一个它失效了。
我们再设置一下edit里面的字体颜色:
当然现在这个是针对所有的edit窗口都改变了,但是我们可以通过参数lParam这个窗口句柄把我们的目标窗口去比对出来的,这里只是告诉大家这个原理,到时候做框架的时候我们会有方法来处理这种情况的。
大家可以自己把这个程序做一个改进:
你可以把这个窗口的背景跟元素的背景的颜色设置成一模一样的,然后当失去焦点的时候呢,就可以把这个窗口隐藏了,或者把这个窗口销毁都可以,就留一个边框,这样子的话就可以达到你用spy++抓不到这个窗口了。
这个edit背景颜色很难看,我们先把设置这个edit的背景颜色代码注释掉:
作业:这个edit窗口的边框用gdi+渐变画刷来做,再用位图画刷来绘制这个边框,最好两个都做。
第十九章 命名规则 匈牙利命名法在win32中,函数名用的跟C#的Pascal命名法差不多,都是首字母大写的驼峰式命名规则,例如PrintName()、GetName()等等;变量名的话就是上面所述的匈牙利命名法。
上堂课的Edit有个bug
就是画框的线条粗细,设置比较大的话,比如4或者5以后:
当把这个边框粗细设置为15的时候,运行程序:
就出现这种情况了,这是什么问题引起的呢?
就是说画线条,gdi+这个笔Pen呀,它画的时候中心线有个对其方式,没设置对齐方式造成的:
这个对其方式默认的时候是0这一种,所以这个中心线就变动了;
那么我们在画线的函数那里要改一下对齐风格:
PenAlignmentInset这种风格自己会计算这个居中的,居中去做这个事的话就没问题了:
它这个笔Pen它有一个对齐方式:
不用自己去算这种中线,用SetAlignment这个函数它可以自己设置:
作业:做一个完整的高大上的QQ登录界面:右上角最小化、关闭,登录按钮、两个用来输入QQ号和密码的Edit,左上角还要有一个标题。
记住密码和自动登录这两个checkbox,跟我们写的按钮差不多,可以自己尝试着写出来(checkbox也是图片替换)。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)