日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

Windows界面UI自绘编程(上)之下部

發布時間:2024/8/1 windows 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Windows界面UI自绘编程(上)之下部 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Windows界面UI自繪編程(上)之下部

    • 第七章 進程
      • 什么叫進程
      • 寫程序,創建進程
      • 仿寫任務管理器
        • 給ListView添加列:
        • 列舉系統進程
        • 接著是內存和路徑的獲取
    • 第八章 Windows進程通訊
      • 通過剪切板進行進程間通訊(新建項目:接收端和發送端)
        • 創建發送端:
        • 再到接收端響應這一個粘貼消息
      • WM_COPYDATA消息是可以跨進程之間發送的
        • 在接收端響應WM_COPYDATA消息
      • 內存共享
    • 第九章 Windows線程
      • 什么是線程
      • 用CreateThread函數創建線程
        • 小實驗1:運行程序彈窗后,在新創建線程還沒有退出的情況下,我們直接關閉主程序。
        • 小實驗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控件加一個邊框:

再創建一個發送按鈕:

響應發送按鈕:

定義一個全局變量,把主窗口引申出來:


所分配的內存并不是堆內存,不需要我們自己釋放、管理,是由系統管理的,系統會自動釋放。

再到接收端響應這一個粘貼消息

在接收端把接收端的主窗口句柄引申出來:

先判斷剪切板里面的數據格式是不是文本格式,我們只接收文本消息,然后打開剪切板、獲取剪切板里面的數據:

我們要在文本框里面插入字符,首先要在光標位置選擇一段空的字符:

WM_COPYDATA消息是可以跨進程之間發送的

由于SendMessage是阻塞式的,該函數執行的時候會在這里一直等待返回,所以該發送端程序不會退出;
不要用PostMessage,因為COPYDATASTRUCT結構里面有指針,PostMessage不是阻塞式的,有可能發送端程序被關閉導致指針所指向的內存失效;
我們一會可以看一下發送端這里的cdata.lpData的地址,然后再看一下接收端那里獲得的數據地址,看這兩者是不是一樣。

在接收端響應WM_COPYDATA消息

可以看到這兩個地址根本就不一樣,所以接收端的COPYDATASTRUCT結構體中的lpData指針所指向的內存是系統重新分配的,這個內存并不是映射。

內存共享

不同進程之間可以分配一塊內存,這一塊內存就像文件一樣,給它取一個名字,各進程通過這個名字可以對這一塊內存進行讀寫;這一種方式可以進行大數據量的交互,效率比較高。

CreateFileMapping函數:

MapViewOfFile函數:

在WinMain函數開始的時候進行一個內存映射文件的初始化:

在WinMain函數結束的時候進行內存映射文件的清理:

然后我們看一下接收端怎么來寫:

我們給接收端創建一個接收的按鈕:

這個程序我們得通過接收端點擊接收按鈕的時候,才能得到發送端傳給共享內存的數據;
內存共享這種方式,我們一般是通過線程來接收,等待這個共享區有數據的時候我們才去讀,沒有的時候,我們就等待;即接收方開一個線程,讓這個線程等待一個事件,等待什么事件呢,在線程同步里面有個“事件”(EVENT),發送方往這個共享區域寫數據的時候就會觸發一個事件,有這個事件了接收方的線程就會去里面讀,否則就一直等待這個事件,這種使用方式等學完線程和線程同步之后再講。


第九章 Windows線程

可以看到這個仿任務管理器的程序,沒有以管理員身份運行,并沒有提權限,就能獲取到內存使用情況。

而且windows操作系統的任務管理器也有的進程路徑獲取不到。

什么是線程

由于線程們共享進程中的資源,所以產生了所謂的線程同步,有所謂的線程鎖。

_beginthreadex函數的實現原理:

用CreateThread函數創建線程

可能會有這種情況:我們的這個創建的線程還沒有運行完,但我們的主程序已經要退出了。
我們要在主程序退出之前,要把這個線程結束了:

小實驗1:運行程序彈窗后,在新創建線程還沒有退出的情況下,我們直接關閉主程序。

這個新創建的線程一直卡在MessageBox這里。

然后我們直接關閉這個主程序(點擊主程序窗口右上角關閉按鈕),看看會出現什么情況:

結果我們這個主程序始終沒有退出,斷點一直沒觸發,執行流程一直沒過來,主程序會一直在WaitForSingleObject這一行等下去;
當點擊彈窗的確定按鈕,WaitForSingleObject函數就返回了,開始執行CloseHandle(g_hThread1)這一句了:

但是我們把等待時間改成5秒鐘:

運行程序后關閉主程序窗口的話,彈窗還在,但是超過5秒鐘后,因為WaitForSingleObject函數所等待的這個新創建的線程還沒結束,超時了,WaitForSingleObject函數返回,然后強行結束這個新創建的線程,該彈窗就退出了。

小實驗2:

_beginthreadex函數

_beginthreadex函數是在哪個頭文件當中呢,我們點擊VS右上角拐彎箭頭找該函數的聲明頭文件:

也是如前面實驗的操作,我們關閉主程序窗口,5秒鐘后該彈窗退出。

進程:在系統里面有個進程控制塊(PCB),是系統進行進程調度的時候,為了進程重入而保存的一個數據結構;
線程:也有一個線程控制塊。

作業:

在這個ReadShareMemory函數里面用一個死循環去讀取數據。


第十章 線程同步

(11:00)臨界區演示代碼

這兩個線程都同時訪問Func_Test這一個函數,這個函數里面就相當于一段共享資源,我們在里面定義一個局部的靜態變量:

當程序退出的時候,線程的釋放工作先不管了,偷個懶(以演示為目的):

這只彈出了一個線程的彈窗,需要修改代碼:

這里出來了兩個線程的彈窗,同一時間產生了這兩個對話框,現在我們是沒有做任何保護措施的;
為了實現同一時間段只有一個線程能夠訪問,我們要使用臨界區:

在程序開始的時候需要初始化一下這個全局的臨界區變量:

在程序退出之前我們要刪除這個臨界區變量:

用臨界區保護共享資源后再運行代碼可以發現只有一個彈窗:

點擊確定關閉這個彈窗后,另外一個彈窗才會出來,即只有前面一個關閉之后下一個才能出來。

信號量

信號量:它是可以跨進程的。

初始的時候可以定義有幾個線程可以同時擁有這個信號量,比如我們定義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即可。

可以看到它也只有一個彈窗,關閉之后又彈出第二個。

信號量可以限制有多個線程可以同時進來訪問這個共享資源的。

事件同步

事件:通知消息的意思,多個線程同時等待一個可以執行的事件通知,事件在同一時刻只能通知一個線程,所以它就達到了一個同步的效果。

事件演示代碼

我們一般手動的事件的用的比較多;
有信號線程才能運行下去。

把事件鎖定,就是讓事件變為無信號狀態,那么其他那些等待線程等不到信號就掛起了;
把事件解鎖,就使得事件變為有信號狀態。

程序測試沒問題。
我們再試一下自動恢復信號的方式:

自動的話就不需要ResetEvent這行代碼了。

互斥量

互斥量算是強化版的臨界區。


原子鎖

這里只講前兩個自加鎖函數和自減鎖函數,后面的自己看百度。

原子鎖代碼演示


這個引用計數就是記錄這個類對象被創建以后,這個對象的指針被引用了多少次。

編譯發現還是報錯:

修改引用計數變量的類型為volatile long才可以,這個volatile long就是對變量做一個可以被多個線程引用的聲明。
InterlockedIncrement和InterlockedDecrement這兩個函數本身就是內部帶鎖的。

這種遞歸調用肯定會溢出(無限制的遞歸調用),這里只是為了說明遞歸調用加鎖是沒有用的,第二次進入該函數執行到WaitForSingleObject函數那行不會等待,而是直接往下面跑;就是說在這種遞歸調用的時候(都是同一個線程),第二次進入該函數,這個鎖加不加都是一個效果了,WaitForSingleObject這行代碼等于就是無效的了;
這種鎖只會鎖不同線程,同一個線程的話是不會鎖的。

我們又寫了一個函數Fun_Test1這樣能夠看的更清楚;
在Fun_Test函數里面調用了這個Fun_Test1函數,在Fun_Test1函數里面加鎖跟沒加是一樣的,因為我們在Fun_Test函數里面已經加了互斥量的鎖,然后調用了Fun_Test1函數,在Fun_Test1函數里面對于同一個互斥量鎖的話, 它這個鎖是無效的了,跟沒加這個鎖是一樣的,它是不會鎖住的。

作業(用事件通知去做數據同步)

進程間通信,用這個事件通知去做一個數據同步;我們接收端線程等待發送端的事件通知,收到這個事件通知后就去讀共享內存,讀出來后把事件再置為有信號。
用線程+事件把共享內存這種進程間通訊方式做的更完美。

第十一章 線程封裝

1. 線程同步對象封裝

把上節課講的5種線程同步方式進行封裝,方便以后我們自己拿來用。

新建一個頭文件Lock.h(我們前面也說了,線程同步本質上來說就是一個鎖):

今天我們用一下命名空間。

1.1 封裝臨界區

1.1.1 我們再定義兩個成員函數(加鎖和解鎖)

1.1.2 利用模板技術來做一個智能鎖

不需要每次自己手動調用Lock()和UnLock()函數,這樣更方便一點,我們利用模板技術來做一個智能鎖,我們只要在開始的時候定義一下就可以了,我們可以更偷懶一點。

這里我們現在只定義了一個臨界區,我們待會要這個智能鎖適應我們其他的線程同步對象(互斥體、信號量、事件),所以這里就用到了模板技術。

在臨界區里面這個Type就是CRITICAL_SECTION這個類型,在其他線程同步方式里面它就是一個句柄HANDLE(線程同步對象);
LockPolicy在臨界區這里它就相當于我們封裝的CriticalSection這個類:

我們可以看到CriticalSection這個類的構造函數是帶了參數的,而我們是傳了CRITICAL_SECTION類型的指針進來的:

現在我們只是把各種同步類型用模板給統一起來了,像什么互斥量、信號量、臨界區,在這個模板里面可以統一調用;
接下來我們還想把Lock()加鎖和UnLock()解鎖這兩步給省略掉。

1.1.3 自動鎖

首先定義一個智能鎖類型的成員變量:

1.1.4 自動鎖使用演示

在主函數中先定義兩個線程句柄和兩個線程函數:

添加一個菜單項:啟動線程

多線程的程序測試框架寫完了,現在還沒加鎖,沒加鎖的時候運行程序會出現兩個彈窗:

我們接下來給它加個鎖。

運行程序測試成功;
我們這個自動鎖是一個局部變量,它的構造函數里面自動調用了加鎖,析構函數里面自動調用了解鎖,這樣子我們就免除了手動調用加鎖和解鎖函數了,這樣子我們就不需要顯式去調用各個線程同步方式里面的那些函數了。

這種重用封裝的概念是C++程序員必須具備的基本能力。

1.2 封裝信號量

因為我們這個信號量是在線程內使用的,不跨進程使用,所以不需要信號量的名字;
當然你也可以改造成跨進程的(通過OpenSemaphore函數),有兩種情形:
1)你指定其中一個進程調用CreateSemaphore函數,其他進程調用OpenSemaphore函數;
2)先調用OpenSemaphore函數,如果失敗,再調用CreateSemaphore函數。

1.2.1 定義鎖定函數和解鎖函數

1.2.2 自動信號量演示

信號量封裝程序測試成功。

作業:事件和互斥量的封裝。

2. 線程的封裝

我們一般還要在主程序結束的時候等待線程的結束:

我們線程中如果有資源的時候,千萬要慎重,不要輕易去調用TerminateThread函數終止線程,我們要保證我們的資源安全釋放了。

我們來做這種線程的封裝,先添加一個頭文件:

定義消息執行回調接口

我們經常有這種需要,windows里面的消息隊列它是把所有消息自定義了一個序列化,有時候我們有這種需要,可能我們的一個線程它在不停地處理各種事件,那么這個時候我們來做一個類似于windows消息機制的一個線程封裝。


我們定義的這個T就是一個消息類型。
我們前面講過的WM_NOTIFY消息中的lParam參數,它用LPNMHDR這樣一個結構體來代表所有的消息:

所以我們可以自定義一個像NMHDR類型的結構體,它可以包含事件的類型、它的參數。

在多態那里學過這種程序接口,但是我們不知道在什么情況下會使用它,這里就來看一下怎么使用抽象類。

我們再定義一個執行返回結果的枚舉類型,我們每一次執行的返回結果,我們看到在消息處理函數WndProc中每一次執行都會返回一個結果:

這樣我們就定義好這個回調接口了。

定義MessageQueue類

首先我們這個線程有一個回調接口IMessageExecute,把這個接口聲明成我們這個類的成員變量,同時我們要執行線程的話,這里面還要有一個線程的句柄:

析構函數里面我們要有一個停止線程的成員函數,并關閉線程句柄:

還要有一個啟動線程函數:

我們在這里等一下看看這個線程執行完了沒有,如果這個線程還在執行的話,代表這個線程已經啟動了,那我們就不需要再去啟動它了。

有沒有這種可能,在啟動線程的時候,這個線程很可能已經在執行了,但也有可能這個線程馬上就會結束,這個線程執行完就會自動停止,所以我們在這里給它加把鎖。

先定義一個自動鎖類型:

如果線程沒啟動的話我們要啟動一個線程,首先得定義一個線程函數(用該線程函數創建線程的時候要記得傳參this指針):

再修改一下啟動線程函數:

定義消息類型


我們定義的這個T就是一個消息類型,類似于WM_NOTIFY消息中lParam參數,它其實是一個NMHDR結構體數據類型的指針,我們來定義一個消息類型,我們使用消息類型來決定CMessageQueue這一個不同的線程。

我們定義一個停止線程的標志m_bStop用來指示線程什么時候退出:

對于m_cMsg這個消息隊列,我們還要有消息的插入函數和彈出函數:

這個bActive是什么意思呢,就是說我們一開始沒有消息的時候,我們的線程是不開啟的;如果有消息進來了,它這個線程如果沒開啟的話就把它開啟起來,就類似于MFC一樣的那種win32消息機制。

我們接下來看一下怎么從消息隊列里面拿消息?

如果執行的回調函數要求你這個線程結束的話(E_MEResult_EXIT),我們消息隊列里面可能還有消息沒有執行完,這些消息里面可能會有指針之類的資源,那么我們要把這些資源釋放了。

我們把所有消息彈出來,把它們釋放了,讓外面去釋放(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()了,那么我們最后還是要判斷一下有沒有需要釋放的資源,有的話還是要把它釋放。

編譯有錯誤,修改代碼如下:

測試程序:

這是一個線程在處理我們這些消息。

我們在上圖所選位置下個斷點,點擊彈窗按鈕關閉后斷到該位置,單步步入:

如上圖所示可以看到,我們得到了從外部傳進來的消息:

然后又來了一個消息,我們又要去執行:

當消息隊列里沒有消息的時候,它會一直在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

(2)鼠標抬起

鼠標一抬起之后,就要退出拖動狀態,釋放鼠標了:

我們之前講MFC的時候說過,消息映射要調用父窗口的消息函數,這就是為什么我們要調用父窗口的消息函數;
父窗口消息函數為我們處理了一些功能相同的窗口事件,因為像拖拽窗口這種功能是所有窗口都一樣的。

(3)鼠標拖拽移動的話窗口也要跟著移動

要記錄鼠標按下的坐標點:

我們要獲取鼠標按下的這個初始的坐標點,然后我們移動到下一個坐標點的時候,跟前一個坐標點要去比較;
計算移動的一個距離,然后與獲取的當前窗口坐標(未移動前)相加得到窗口新的坐標點:

作業:


第十四章 自制界面庫之Windows窗口自繪(二)

上節課的程序有個BUG:
鼠標移動的時候沒有記錄上一次的坐標點,所以鼠標一點之后窗口就不見了。

這個Demo是一個測試程序,

我們鼠標按住上圖紅色方塊所在那行的話,是可以拖動窗口的;但是按住紅色方塊下方區域是不能拖動的。

如果我們把Create的風格參數從WS_OVERLAPPEDWINDOW改為WS_POPUP的話,窗口就變成了:

即top頂端0到bottom底端100這個范圍內可以拖動窗口。

窗口消息

這些是窗口自繪常用的消息。

WM_NCCREATE

是窗口創建出來之后的第一個消息(窗口創建成功之后只產生一次這個消息),它告訴你窗口創建成功了,它是窗口創建成功之后發送的第一個消息,這個是窗口創建的時候系統自動產生的;

WM_CREATE:窗口創建之后的第二個消息(窗口創建成功之后只產生一次這個消息),我們一般在這里面進行窗口界面元素的初始化(子窗口的創建、窗口控件的創建);
windows的控件都是窗口,它們也會產生跟窗口一樣的消息(WM_NCCREATE、WM_CREATE);

WM_DESTROY

窗口銷毀消息;

我們要把WM_DESTROY這個事件映射出來的原因是,方便我們在窗口銷毀之后進行一些資源的釋放;另外像我們主窗口銷毀的話,我們應該退出應用程序。

退出應用程序,我們主窗口就銷毀了。

WM_SIZE

窗口大小產生變化后的一個通知消息,它與窗口實際大小的變化沒關系;
我們在WM_CREATE消息那里創建子窗口或者窗口控件的時候,控件是不是有在窗口內的坐標,我們在這個消息里面調整控件的坐標,來適應窗口大小的變化,進行控件的重新布局來適應這個窗口;

我們在從CXUIWnd類繼承的CMainFrame主窗口類中創建了一個CXUIWnd類型的子窗口,上圖的OnCreate消息處理函數這里創建了一個子窗口(用spy++查看可以驗證):

我們改下這個子窗口,讓它自動隨著主窗口的大小變化而變化:

// MainFrame.cpp:

我們來給XUIWnd.h增加一個函數:

編譯運行程序,我們拖動邊框改變窗口大小,可以看到我們這個子窗口與主窗口始終相差10個單位,這個函數OnSize主要做這個事情的(重新布局)。

WM_ACTIVATE和WM_SETFOCUS

WM_ACTIVATE:窗口在激活(當前窗口)與非激活之間。
WM_SETFOCUS:這個是獲取鍵盤輸入焦點;焦點可能在當前窗口的任何控件上面。
WM_ACTIVATE和WM_SETFOCUS還是有區別的,焦點不一定在激活的窗口上面。
我們在窗口自繪的時候會用到WM_ACTIVATE,因為我們在窗口一激活的時候,我們要處理這個焦點的問題,到時候可能要在WM_ACTIVATE這里面做一些事情。


這個就先打個樁。

WM_KILLFOCUS:窗口失去焦點。
WM_SETFOCUS:窗口獲取到焦點。

WM_ENABLE

窗口被禁止或允許接收鼠標、鍵盤消息。
我們應該用過這個函數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這些鍵會產生。

窗口自繪

自己做皮膚庫、界面庫的話,我們怎么來自繪這個窗口呢?

一般我們自繪的話,會把窗口的風格改為沒有標題欄,沒有邊框的窗口:

我們先把原來寫的程序中的子窗口去掉:

我們要在上圖這個窗口上自己來繪制標題欄,怎么來繪呢?
有兩種情況:

1)窗口固定大小的話,我們直接只要一張跟窗口一樣大小的背景圖就可以了;

先給這個窗口來一張背景位圖:

其實我們可以把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生成解決方案)才能夠編譯過去。

我們先來感受下這個已經寫出來的按鈕

先把按鈕上的文字去掉,我們先不繪文字:

把上圖所選這行刪除掉:

我們先來感受下自己繪制的這個按鈕,鼠標移上去按鈕是一個效果,點擊不松的時候又是一種效果,它都有不同的效果;
現在點擊按鈕沒反應,我們修改代碼的注釋符如下:

此時點擊按鈕后會彈窗。

按鈕特性

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重載這個事件控制的函數:

WM_MOUSEMOVE

第一個就是鼠標移上去的事件,我們要判斷一下鼠標點是否落在我們的按鈕上面,如果它不在按鈕上面我們就不需要去管它了。

這個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):

接下來打開繪制和事件響應:

運行程序后發生崩潰,單步調試代碼發現問題,把下圖所選刪除:

我們點擊關閉按鈕后,就彈出一個窗口,我們需要點擊關閉按鈕后退出應用程序:

這樣子按鈕的重繪就完成了。

我們把另一個按鈕的代碼也打開,我們一共做了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控件做了一些特殊的處理。

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也是圖片替換)。

總結

以上是生活随笔為你收集整理的Windows界面UI自绘编程(上)之下部的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。