本系列文章由Tangram開發團隊編寫。Tangram是咱們開發的一套面向Windows桌面的軟件膠水技術。基於Tangram,開發者能夠以一種全新的方式來構造桌面軟件。Tangram自己是一個複雜的概念,咱們但願經過本系列文章讓讀者真正的瞭解Tangram的思想。Tangram沒有特定的語言限制,不管你是C++開發者,Java開發者仍是.Net開發者。均可以從Tangram技術中獲益。爲了更方便的解釋,下文中咱們將從一個最簡單的Win32應用程序開始逐步展現出Tangram的魅力。c++
桌面開發技術發展到今天,已經有許多簡單快捷的方式讓開發者輕鬆的構建桌面軟件。若是你是一名C++開發者,你能夠基於MFC開發桌面應用。若是你是一名.Net開發者,你能夠基於WinForm技術開發基於控件的標準窗體程序。Java也有相似的技術,例如:SWT,SWING等。或者,你可使用WPF基於XAML構建更富有想象力的程序界面。若是你是Web開發者,你也可使用Electron開發基於HTML的Hybrid應用程序。這些技術各有各的優點和劣勢。人們每每在權衡利弊以後,從中選擇最適合本身的一種。但這些就是桌面開發的所有嗎?咱們說並非。爲了向你們展現這一點,讓咱們回到一切的開端。編程
Win32 API,這幾乎是全部Windows開發技術的基礎。在20年前,大多數VC++開發者都是使用這套API來構建桌面軟件的。今天,咱們將從新建立一個全新的Win32 工程,一步步的構建咱們心目中的軟件系統。windows
爲了完成咱們的演示,你須要一套最新的Visual Studio開發環境。在這裏,咱們是使用Visual Studio 2017 Enterprise版本,你可使用Community或者Professional版本,這都沒有問題。新版本的Visual Studio使用可選的方式讓開發者選擇本身須要的組件。在這裏,你須要app
而後,讓咱們建立第一個全新的Win32工程,咱們選擇Visual C++ > Windows Desktop > Windows Desktop Application編輯器
Wizard默認爲咱們建立了一個空白窗口,這是一個Windows頂層窗口。函數
咱們經過Visual Studio > Tools > Spy++ 解析這個窗口ui
能夠查看這個窗口的基本信息,其中005604EE是它的窗口句柄,你那裏可能有所不一樣。這裏引出了Windows開發的核心概念,Window對象。在Microsoft的設計中,Windows中的全部可見和不可見元素幾乎都是由Window對象直接或間接構成的。你可使用Spy++中的望遠鏡在你的Windows桌面上掃描幾回。你會發現那些形形色色的窗口,圖標,按鈕本質上都是Window對象。那麼,咱們就來建立第一個咱們本身的Window對象。spa
每一個Window都須要一個ClassName和Title操作系統
WCHAR szChildWindowTitle[] = TEXT("Win32Launcher Child Window"); // the child window title bar text WCHAR szChildWindowClass[] = TEXT("Win32Launcher Child Window"); // the child window class name
咱們須要使用ClassName向系統註冊這個窗口,爲了更好的區分,咱們使用COLOR_HIGHLIGHT做爲Window的背景色設計
// // FUNCTION: RegisterChildWindowClass() // // PURPOSE: Registers the child window class. // ATOM RegisterChildWindowClass(HINSTANCE hInstance) { WNDCLASSEXW wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = ChildWindowProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WIN32LAUNCHER)); wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_HIGHLIGHT); wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WIN32LAUNCHER); wcex.lpszClassName = szChildWindowClass; wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); return RegisterClassExW(&wcex); }
每一個Window都須要一個WndProc函數來處理髮往該Window對象的消息,若是你沒有額外的處理需求,能夠直接調用默認處理函數DefWindowProc
// Message handler for child window LRESULT CALLBACK ChildWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { return DefWindowProc(hWnd, message, wParam, lParam); }
咱們計劃讓這個Window填滿主窗口的客戶區域。咱們須要首先獲取主窗口的客戶區域尺寸
// Get the size of main window client area RECT rc; ::GetClientRect(hWnd, &rc);
而後使用獲取的尺寸信息建立咱們的新Window
// Create a child window and populate the main window client area hChildWnd = CreateWindowW(szChildWindowClass, szChildWindowTitle, WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS, 0, 0, rc.right - rc.left, rc.bottom - rc.top, hWnd, NULL, hInstance, NULL); // display the child window ShowWindow(hChildWnd, nCmdShow); UpdateWindow(hChildWnd);
爲了確保新Window的尺寸可以隨着主窗口尺寸的變化而相應的變化,咱們須要額外處理主窗口的WM_WINDOWPOSCHANGED事件,而且在事件處理中相應的調整新Window的尺寸
case WM_WINDOWPOSCHANGED: { // Update the size of the child window when the size of main window // is changed. WINDOWPOS* lpwndpos = (WINDOWPOS*)lParam; if (IsWindow(hChildWnd)) { RECT rc; ::GetClientRect(hWnd, &rc); SetWindowPos(hChildWnd, HWND_BOTTOM, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOACTIVATE | SWP_NOREDRAW); } return DefWindowProc(hWnd, message, wParam, lParam); } break;
咱們再次運行工程,將會看到一個灰色的窗口填滿了原有窗口客戶區域。
再次使用Spy++觀察這個區域
咱們看到,原有的主窗口下面添加了一個咱們新建的子窗口。嘗試調整主窗口尺寸,你會觀察到子窗口尺寸跟隨着變化。
在現實的應用場景中,一個應用程序窗口都是由許多不一樣的功能區域構成的。以Visual Studio爲例,有編輯器區域,解決方案面板,輸出面板,屬性面板等。考慮多個窗口的狀況,讓咱們再額外建立一個窗口,讓兩個子窗口左右對齊排列。
這裏咱們定義兩個窗口句柄,爲了美觀,咱們讓兩個窗口之間有4個像素的間隙。
HWND hLChildWnd; // the left child window handle HWND hRChildWnd; // the right child window handle LONG lGutterWidth = 4; // the gutter width
這裏咱們將左側窗口的寬度設爲(rc.right - rc.left - lGutterWidth) / 2
// Get the size of main window client area RECT rc; ::GetClientRect(hWnd, &rc); // Create a child window on the left hLChildWnd = CreateWindowW(szChildWindowClass, szChildWindowTitle, WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS, 0, 0, (rc.right - rc.left - lGutterWidth) / 2, rc.bottom - rc.top, hWnd, NULL, hInstance, NULL);
同理,右側窗口也作相應的調整
// Create a child window on the right hRChildWnd = CreateWindowW(szChildWindowClass, szChildWindowTitle, WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS, (rc.right - rc.left - lGutterWidth) / 2 + lGutterWidth, 0, (rc.right - rc.left - lGutterWidth) / 2, rc.bottom - rc.top, hWnd, NULL, hInstance, NULL);
咱們也須要在主窗口尺寸更新時調整子窗口的尺寸
case WM_WINDOWPOSCHANGED: { // Calculate the size of all child windows when the main window // size is changed. WINDOWPOS* lpwndpos = (WINDOWPOS*)lParam; if (IsWindow(hLChildWnd) && IsWindow(hRChildWnd)) { RECT rc; ::GetClientRect(hWnd, &rc); SetWindowPos(hLChildWnd, HWND_BOTTOM, 0, 0, (rc.right - rc.left - lGutterWidth) / 2, rc.bottom - rc.top, SWP_NOACTIVATE | SWP_NOREDRAW); SetWindowPos(hRChildWnd, HWND_BOTTOM, (rc.right - rc.left - lGutterWidth) / 2 + lGutterWidth, 0, (rc.right - rc.left - lGutterWidth) / 2, rc.bottom - rc.top, SWP_NOACTIVATE | SWP_NOREDRAW); } return DefWindowProc(hWnd, message, wParam, lParam); } break;
再次運行程序,咱們將看到
爲了加深Window概念的理解,咱們在場景二的基礎上再加深一層。此次,咱們建立一個1/2窗口和兩個1/4窗口。
咱們建立3個窗口句柄
HWND hLChildWnd; // the left child window handle HWND hURChildWnd; // the upper right child window handle HWND hLRChildWnd; // the lower right child window handle LONG lGutterWidth = 4; // the gutter width
對於以前的右側窗口,咱們替換成上下兩個窗口。首先建立右上的窗口
// Create a upper right child window hURChildWnd = CreateWindowW(szChildWindowClass, szChildWindowTitle, WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS, (rc.right - rc.left - lGutterWidth) / 2 + lGutterWidth, 0, (rc.right - rc.left - lGutterWidth) / 2, (rc.bottom - rc.top - lGutterWidth) / 2, hWnd, NULL, hInstance, NULL);
接着咱們建立右下角的窗口
// Create a lower right child window hLRChildWnd = CreateWindowW(szChildWindowClass, szChildWindowTitle, WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS, (rc.right - rc.left - lGutterWidth) / 2 + lGutterWidth, (rc.bottom - rc.top - lGutterWidth) / 2 + lGutterWidth, (rc.right - rc.left - lGutterWidth) / 2, (rc.bottom - rc.top - lGutterWidth) / 2, hWnd, NULL, hInstance, NULL);
同理在WM_WINDOWPOSCHANGED中對窗口做出調整
case WM_WINDOWPOSCHANGED: { // Calculate the size of all child windows when the main window // size is changed. WINDOWPOS* lpwndpos = (WINDOWPOS*)lParam; if (IsWindow(hLChildWnd) && IsWindow(hURChildWnd) && IsWindow(hLRChildWnd)) { RECT rc; ::GetClientRect(hWnd, &rc); SetWindowPos(hLChildWnd, HWND_BOTTOM, 0, 0, (rc.right - rc.left - lGutterWidth) / 2, rc.bottom - rc.top, SWP_NOACTIVATE | SWP_NOREDRAW); SetWindowPos(hURChildWnd, HWND_BOTTOM, (rc.right - rc.left - lGutterWidth) / 2 + lGutterWidth, 0, (rc.right - rc.left - lGutterWidth) / 2, (rc.bottom - rc.top - lGutterWidth) / 2, SWP_NOACTIVATE | SWP_NOREDRAW); SetWindowPos(hLRChildWnd, HWND_BOTTOM, (rc.right - rc.left - lGutterWidth) / 2 + lGutterWidth, (rc.bottom - rc.top - lGutterWidth) / 2 + lGutterWidth, (rc.right - rc.left - lGutterWidth) / 2, (rc.bottom - rc.top - lGutterWidth) / 2, SWP_NOACTIVATE | SWP_NOREDRAW); } return DefWindowProc(hWnd, message, wParam, lParam); } break;
運行程序,咱們看到
在以前的場景中,咱們都假設窗口被平均的切分。如今咱們試圖讓左側的窗口擁有固定的寬度。 咱們將它的寬度設置爲200像素。
HWND hLChildWnd; // the left child window handle HWND hURChildWnd; // the upper right child window handle HWND hLRChildWnd; // the lower right child window handle LONG lLChildWndWidth = 200; // the left child window width LONG lGutterWidth = 4; // the gutter width
此次,爲了美觀,咱們爲窗口設置不一樣的背景顏色。爲此,咱們須要註冊3個不一樣的窗口類
WCHAR szRedWindowClass[] = TEXT("Win32Launcher Red Window"); // the red child window class name WCHAR szOrangeWindowClass[] = TEXT("Win32Launcher Orange Window"); // the orange child window class name WCHAR szGreenWindowClass[] = TEXT("Win32Launcher Green Window"); // the green child window class name
咱們修改原來的窗口註冊函數,讓它可以支持不一樣的背景顏色
// // FUNCTION: RegisterChildWindowClass() // // PURPOSE: Registers the child window class with special background color. // ATOM RegisterChildWindowClass(HINSTANCE hInstance, LPCWSTR lpClassName, COLORREF color) { WNDCLASSEXW wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = ChildWindowProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WIN32LAUNCHER)); wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); wcex.hbrBackground = CreateSolidBrush(color); wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WIN32LAUNCHER); wcex.lpszClassName = lpClassName; wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); return RegisterClassExW(&wcex); }
註冊這些窗口
RegisterChildWindowClass(hInstance, szRedWindowClass, 0x004d5adc); RegisterChildWindowClass(hInstance, szOrangeWindowClass, 0x0035befe); RegisterChildWindowClass(hInstance, szGreenWindowClass, 0x009cb14b);
使用固定的寬度建立左側的窗口
// Create a child window on the left hLChildWnd = CreateWindowW(szRedWindowClass, szLWindowTitle, WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS, 0, 0, lLChildWndWidth, rc.bottom - rc.top, hWnd, NULL, hInstance, NULL);
建立右側的兩個窗口
// Create a upper right child window hURChildWnd = CreateWindowW(szOrangeWindowClass, szURWindowTitle, WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS, lLChildWndWidth + lGutterWidth, 0, (rc.right - rc.left) - lLChildWndWidth - lGutterWidth, (rc.bottom - rc.top - lGutterWidth) / 2, hWnd, NULL, hInstance, NULL); // display the upper right child window ShowWindow(hURChildWnd, nCmdShow); UpdateWindow(hURChildWnd); // Create a lower right child window hLRChildWnd = CreateWindowW(szGreenWindowClass, szLRWindowTitle, WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS, lLChildWndWidth + lGutterWidth, (rc.bottom - rc.top - lGutterWidth) / 2 + lGutterWidth, (rc.right - rc.left) - lLChildWndWidth - lGutterWidth, (rc.bottom - rc.top - lGutterWidth) / 2, hWnd, NULL, hInstance, NULL); // display the lower right child window ShowWindow(hLRChildWnd, nCmdShow); UpdateWindow(hLRChildWnd);
固定寬度意味着主窗口尺寸改變時,仍然保持不變的寬度
case WM_WINDOWPOSCHANGED: { // Calculate the size of all child windows when the main window // size is changed. WINDOWPOS* lpwndpos = (WINDOWPOS*)lParam; if (IsWindow(hLChildWnd) && IsWindow(hURChildWnd) && IsWindow(hLRChildWnd)) { RECT rc; ::GetClientRect(hWnd, &rc); SetWindowPos(hLChildWnd, HWND_BOTTOM, 0, 0, lLChildWndWidth, rc.bottom - rc.top, SWP_NOACTIVATE | SWP_NOREDRAW); SetWindowPos(hURChildWnd, HWND_BOTTOM, lLChildWndWidth + lGutterWidth, 0, (rc.right - rc.left) - lLChildWndWidth - lGutterWidth, (rc.bottom - rc.top - lGutterWidth) / 2, SWP_NOACTIVATE | SWP_NOREDRAW); SetWindowPos(hLRChildWnd, HWND_BOTTOM, lLChildWndWidth + lGutterWidth, (rc.bottom - rc.top - lGutterWidth) / 2 + lGutterWidth, (rc.right - rc.left) - lLChildWndWidth - lGutterWidth, (rc.bottom - rc.top - lGutterWidth) / 2, SWP_NOACTIVATE | SWP_NOREDRAW); } return DefWindowProc(hWnd, message, wParam, lParam); } break;
爲了更好的標識每一個窗口,咱們使用繪圖API將每一個窗口的標題文字繪製到窗口上。每當操做系統認爲當前窗口須要從新繪製時,都會觸發該WM_PAINT消息。
// Message handler for child window LRESULT CALLBACK ChildWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); // Draw the WindowTitle text onto the window. int length = GetWindowTextLengthW(hWnd) + 1; LPWSTR lpWindowTitle = new WCHAR[length]; GetWindowTextW(hWnd, lpWindowTitle, length); RECT rc; GetClientRect(hWnd, &rc); SetTextColor(hdc, 0x00ffffff); SetBkMode(hdc, TRANSPARENT); rc.left = 10; rc.top = 10; DrawText(hdc, lpWindowTitle, -1, &rc, DT_SINGLELINE | DT_NOCLIP); delete lpWindowTitle; EndPaint(hWnd, &ps); } break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }
讓咱們看一下添加了背景色以後的窗口
嘗試改變主窗口的尺寸,你會觀察到左側窗口依舊保持相同的寬度。經過Spy++檢查一下窗口結構
右鍵菜單選擇屬性,查看一下窗口的寬度
上文中最多隻建立了3個子窗口,已經產生了尺寸問題。那麼更加複雜的窗口結構該如何建立呢?這裏咱們引出一種參數化的建立思路。假設咱們須要建立一種循環結構。將主窗口分爲左右兩個子窗口,將右側的子窗口轉換爲上下兩個子窗口。將上面的子窗口再次分紅左右兩個子窗口。依此類推。爲此咱們須要一種遞歸結構。
void RecursivelyCreateWindow(HINSTANCE hInstance, int nCmdShow, HWND hPWnd, int nPosIndex, int nLevel) { WCHAR* szWindowClass = NULL; int x, y, nWidth, nHeight; // Get the size of parent window client area RECT rc; ::GetClientRect(hPWnd, &rc); switch (nPosIndex) { case 1: szWindowClass = szRedWindowClass; x = rc.left; y = rc.top; nWidth = (rc.right - rc.left - lGutterWidth) / 2; nHeight = rc.bottom - rc.top; break; case 2: szWindowClass = szOrangeWindowClass; x = (rc.right - rc.left - lGutterWidth) / 2 + lGutterWidth; y = rc.top; nWidth = (rc.right - rc.left - lGutterWidth) / 2; nHeight = rc.bottom - rc.top; break; case 3: szWindowClass = szGrayWindowClass; x = rc.left; y = rc.top; nWidth = rc.right - rc.left; nHeight = (rc.bottom - rc.top - lGutterWidth) / 2; break; case 4: szWindowClass = szGreenWindowClass; x = rc.left; y = (rc.bottom - rc.top - lGutterWidth) / 2 + lGutterWidth; nWidth = rc.right - rc.left; nHeight = (rc.bottom - rc.top - lGutterWidth) / 2; break; } HWND hWnd = CreateWindowW(szWindowClass, szChildWindowTitle, WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS, x, y, nWidth, nHeight, hPWnd, NULL, hInstance, NULL); // display the window ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); if (nLevel < 6) { if (nPosIndex == 2) { RecursivelyCreateWindow(hInstance, nCmdShow, hWnd, 3, nLevel + 1); RecursivelyCreateWindow(hInstance, nCmdShow, hWnd, 4, nLevel + 1); } else if (nPosIndex == 3) { RecursivelyCreateWindow(hInstance, nCmdShow, hWnd, 1, nLevel + 1); RecursivelyCreateWindow(hInstance, nCmdShow, hWnd, 2, nLevel + 1); } } mapWindows[hWnd] = nPosIndex; }
其中hPWnd是待建立子窗口的父窗口。nPosIndex是位置的索引,1,2,3,4分別表明左,右,上,下。代碼基於這個索引值決定如何在當前父窗口下進行切分。nLevel是遞歸的層數。與上文中的其它案例不一樣。爲了方便定位,咱們會建立一些僅僅用於定位的容器窗口。
爲了後續的尺寸更新,咱們須要保存全部建立的窗口句柄。這裏咱們創建了一個map結構。
std::map<HWND, int> mapWindows; // the mapping between the window handle and the position index
在WM_WINDOWPOSCHANGED中,咱們須要遍歷主窗口下的全部子窗口,並更新它們的尺寸。
case WM_WINDOWPOSCHANGED: { // Calculate the size of all child windows when the parent window // size is changed. WINDOWPOS* lpwndpos = (WINDOWPOS*)lParam; // Recursively update the child window position. EnumChildWindows(hWnd, UpdateWindowPos, NULL); return DefWindowProc(hWnd, message, wParam, lParam); } break;
EnumChildWindows須要一個Callback函數,這裏一樣存在遞歸邏輯。
// Recursively update the child window position. BOOL CALLBACK UpdateWindowPos(_In_ HWND hWnd, _In_ LPARAM lParam) { std::map<HWND, int>::iterator it = mapWindows.find(hWnd); if (it != mapWindows.end()) { int nPosIndex = it->second; HWND hPWnd = ::GetParent(hWnd); if (IsWindow(hPWnd)) { RECT rc; ::GetClientRect(hPWnd, &rc); WCHAR* szWindowClass = NULL; int x, y, nWidth, nHeight; switch (nPosIndex) { case 1: szWindowClass = szRedWindowClass; x = rc.left; y = rc.top; nWidth = (rc.right - rc.left - lGutterWidth) / 2; nHeight = rc.bottom - rc.top; break; case 2: szWindowClass = szOrangeWindowClass; x = (rc.right - rc.left - lGutterWidth) / 2 + lGutterWidth; y = rc.top; nWidth = (rc.right - rc.left - lGutterWidth) / 2; nHeight = rc.bottom - rc.top; break; case 3: szWindowClass = szGrayWindowClass; x = rc.left; y = rc.top; nWidth = rc.right - rc.left; nHeight = (rc.bottom - rc.top - lGutterWidth) / 2; break; case 4: szWindowClass = szGreenWindowClass; x = rc.left; y = (rc.bottom - rc.top - lGutterWidth) / 2 + lGutterWidth; nWidth = rc.right - rc.left; nHeight = (rc.bottom - rc.top - lGutterWidth) / 2; break; } SetWindowPos(hWnd, HWND_BOTTOM, x, y, nWidth, nHeight, SWP_NOACTIVATE | SWP_NOREDRAW); } EnumChildWindows(hWnd, UpdateWindowPos, lParam); } return TRUE; }
運行程序,讓咱們看看最後實現的結果
是否有一種智力拼圖的感受?讓咱們再次使用Spy++觀察一下窗口結構
這裏咱們就會發現,實際建立的窗口要比視覺上展現的窗口要多。那些額外建立的窗口就是上文所說的容器窗口,它們的職責主要是用於定位。固然,在現實開發中,並不存在如此有規律的嵌套結構。大多數狀況,問題要比這複雜的多。這個例子僅僅向讀者展現了窗口建立的不一樣可能方法和其中的複雜度。在相對混亂的對象中找出規律造成通用解決方案是一種基本的編程技巧。Tangram在此處給出了一種更加靈活高效的組織方法。但在介紹這種組織方法以前,咱們但願讀者瞭解另一些知識點。