本系列文章由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在此处给出了一种更加灵活高效的组织方法。但在介绍这种组织方法以前,咱们但愿读者了解另一些知识点。