热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

基于Tangram开发下一代桌面应用(1)

本系列文章由Tangram开发团队编写。Tangram是我们开发的一套面向Windows桌面的软件胶水技术。基于Tangram,开发者可以以一种全新的方式来构造桌面软

本系列文章由Tangram开发团队编写。Tangram是我们开发的一套面向Windows桌面的软件胶水技术。基于Tangram,开发者可以以一种全新的方式来构造桌面软件。Tangram本身是一个复杂的概念,我们希望通过本系列文章让读者真正的了解Tangram的思想。Tangram没有特定的语言限制,无论你是C++开发者,Java开发者还是.Net开发者。都可以从Tangram技术中获益。为了更方便的解释,下文中我们将从一个最简单的Win32应用程序开始逐步展示出Tangram的魅力。

第一节:全新的开始

桌面开发技术发展到今天,已经有许多简单快捷的方式让开发者轻松的构建桌面软件。如果你是一名C++开发者,你可以基于MFC开发桌面应用。如果你是一名.Net开发者,你可以基于WinForm技术开发基于控件的标准窗体程序。Java也有类似的技术,例如:SWT,SWING等。或者,你可以使用WPF基于XAML构建更富有想象力的程序界面。如果你是Web开发者,你也可以使用Electron开发基于HTML的Hybrid应用程序。这些技术各有各的优势和劣势。人们往往在权衡利弊之后,从中选择最适合自己的一种。但这些就是桌面开发的全部吗?我们说并不是。为了向大家展示这一点,让我们回到一切的开端。

Win32 API,这几乎是所有Windows开发技术的基础。在20年前,大多数VC++开发者都是使用这套API来构建桌面软件的。今天,我们将重新创建一个全新的Win32 工程,一步步的构建我们心目中的软件系统。

为了完成我们的演示,你需要一套最新的Visual Studio开发环境。在这里,我们是使用Visual Studio 2017 Enterprise版本,你可以使用Community或者Professional版本,这都没有问题。新版本的Visual Studio使用可选的方式让开发者选择自己需要的组件。在这里,你需要

  • .NET desktop development
  • Desktop development with C++
  • Visual C++ MFC for x86 and x64
  • C++/CLI support
  • Office/SharePoint development

场景一

然后,让我们创建第一个全新的Win32工程,我们选择Visual C++ > Windows Desktop > Windows Desktop Application

Wizard默认为我们创建了一个空白窗口,这是一个Windows顶层窗口。

我们通过Visual Studio > Tools > Spy++ 解析这个窗口

可以查看这个窗口的基本信息,其中005604EE是它的窗口句柄,你那里可能有所不同。这里引出了Windows开发的核心概念,Window对象。在Microsoft的设计中,Windows中的所有可见和不可见元素几乎都是由Window对象直接或间接构成的。你可以使用Spy++中的望远镜在你的Windows桌面上扫描几次。你会发现那些形形色色的窗口,图标,按钮本质上都是Window对象。那么,我们就来创建第一个我们自己的Window对象。

每个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 areaRECT rc;::GetClientRect(hWnd, &rc);

然后使用获取的尺寸信息创建我们的新Window

// Create a child window and populate the main window client areahChildWnd = 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 windowShowWindow(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 areaRECT rc;::GetClientRect(hWnd, &rc);// Create a child window on the lefthLChildWnd = 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 righthRChildWnd = 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 windowhURChildWnd = 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 windowhLRChildWnd = 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 lefthLChildWnd = 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 windowhURChildWnd = 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 windowShowWindow(hURChildWnd, nCmdShow);UpdateWindow(hURChildWnd);// Create a lower right child windowhLRChildWnd = 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 windowShowWindow(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 &#61; NULL;int x, y, nWidth, nHeight;// Get the size of parent window client areaRECT rc;::GetClientRect(hPWnd, &rc);switch (nPosIndex){case 1:szWindowClass &#61; szRedWindowClass;x &#61; rc.left;y &#61; rc.top;nWidth &#61; (rc.right - rc.left - lGutterWidth) / 2;nHeight &#61; rc.bottom - rc.top;break;case 2:szWindowClass &#61; szOrangeWindowClass;x &#61; (rc.right - rc.left - lGutterWidth) / 2 &#43; lGutterWidth;y &#61; rc.top;nWidth &#61; (rc.right - rc.left - lGutterWidth) / 2;nHeight &#61; rc.bottom - rc.top;break;case 3:szWindowClass &#61; szGrayWindowClass;x &#61; rc.left;y &#61; rc.top;nWidth &#61; rc.right - rc.left;nHeight &#61; (rc.bottom - rc.top - lGutterWidth) / 2;break;case 4:szWindowClass &#61; szGreenWindowClass;x &#61; rc.left;y &#61; (rc.bottom - rc.top - lGutterWidth) / 2 &#43; lGutterWidth;nWidth &#61; rc.right - rc.left;nHeight &#61; (rc.bottom - rc.top - lGutterWidth) / 2;break;}HWND hWnd &#61; CreateWindowW(szWindowClass, szChildWindowTitle,WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPSIBLINGS, x, y, nWidth, nHeight, hPWnd, NULL, hInstance, NULL);// display the windowShowWindow(hWnd, nCmdShow);UpdateWindow(hWnd);if (nLevel <6){if (nPosIndex &#61;&#61; 2){RecursivelyCreateWindow(hInstance, nCmdShow, hWnd, 3, nLevel &#43; 1);RecursivelyCreateWindow(hInstance, nCmdShow, hWnd, 4, nLevel &#43; 1);}else if (nPosIndex &#61;&#61; 3){RecursivelyCreateWindow(hInstance, nCmdShow, hWnd, 1, nLevel &#43; 1);RecursivelyCreateWindow(hInstance, nCmdShow, hWnd, 2, nLevel &#43; 1);}}mapWindows[hWnd] &#61; nPosIndex;
}

其中hPWnd是待创建子窗口的父窗口。nPosIndex是位置的索引&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;4分别代表左&#xff0c;右&#xff0c;上&#xff0c;下。代码基于这个索引值决定如何在当前父窗口下进行切分。nLevel是递归的层数。与上文中的其它案例不同。为了方便定位&#xff0c;我们会创建一些仅仅用于定位的容器窗口。

为了后续的尺寸更新&#xff0c;我们需要保存所有创建的窗口句柄。这里我们建立了一个map结构。

std::map mapWindows; // the mapping between the window handle and the position index

在WM_WINDOWPOSCHANGED中&#xff0c;我们需要遍历主窗口下的所有子窗口&#xff0c;并更新它们的尺寸。

case WM_WINDOWPOSCHANGED:{// Calculate the size of all child windows when the parent window // size is changed.WINDOWPOS* lpwndpos &#61; (WINDOWPOS*)lParam;// Recursively update the child window position.EnumChildWindows(hWnd, UpdateWindowPos, NULL);return DefWindowProc(hWnd, message, wParam, lParam);}break;

EnumChildWindows需要一个Callback函数&#xff0c;这里同样存在递归逻辑。

// Recursively update the child window position.
BOOL CALLBACK UpdateWindowPos(_In_ HWND hWnd, _In_ LPARAM lParam)
{std::map::iterator it &#61; mapWindows.find(hWnd);if (it !&#61; mapWindows.end()){int nPosIndex &#61; it->second;HWND hPWnd &#61; ::GetParent(hWnd);if (IsWindow(hPWnd)){RECT rc;::GetClientRect(hPWnd, &rc);WCHAR* szWindowClass &#61; NULL;int x, y, nWidth, nHeight;switch (nPosIndex){case 1:szWindowClass &#61; szRedWindowClass;x &#61; rc.left;y &#61; rc.top;nWidth &#61; (rc.right - rc.left - lGutterWidth) / 2;nHeight &#61; rc.bottom - rc.top;break;case 2:szWindowClass &#61; szOrangeWindowClass;x &#61; (rc.right - rc.left - lGutterWidth) / 2 &#43; lGutterWidth;y &#61; rc.top;nWidth &#61; (rc.right - rc.left - lGutterWidth) / 2;nHeight &#61; rc.bottom - rc.top;break;case 3:szWindowClass &#61; szGrayWindowClass;x &#61; rc.left;y &#61; rc.top;nWidth &#61; rc.right - rc.left;nHeight &#61; (rc.bottom - rc.top - lGutterWidth) / 2;break;case 4:szWindowClass &#61; szGreenWindowClass;x &#61; rc.left;y &#61; (rc.bottom - rc.top - lGutterWidth) / 2 &#43; lGutterWidth;nWidth &#61; rc.right - rc.left;nHeight &#61; (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;
}

运行程序&#xff0c;让我们看看最后实现的结果

是否有一种智力拼图的感觉&#xff1f;让我们再次使用Spy&#43;&#43;观察一下窗口结构

这里我们就会发现&#xff0c;实际创建的窗口要比视觉上展示的窗口要多。那些额外创建的窗口就是上文所说的容器窗口&#xff0c;它们的职责主要是用于定位。当然&#xff0c;在现实开发中&#xff0c;并不存在如此有规律的嵌套结构。大多数情况&#xff0c;问题要比这复杂的多。这个例子仅仅向读者展示了窗口创建的不同可能方法和其中的复杂度。在相对混乱的对象中找出规律形成通用解决方案是一种基本的编程技巧。Tangram在此处给出了一种更加灵活高效的组织方法。但在介绍这种组织方法之前&#xff0c;我们希望读者了解另外一些知识点。



推荐阅读
  • 表单代码 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 成功安装Sabayon Linux在thinkpad X60上的经验分享
    本文分享了作者在国庆期间在thinkpad X60上成功安装Sabayon Linux的经验。通过修改CHOST和执行emerge命令,作者顺利完成了安装过程。Sabayon Linux是一个基于Gentoo Linux的发行版,可以将电脑快速转变为一个功能强大的系统。除了作为一个live DVD使用外,Sabayon Linux还可以被安装在硬盘上,方便用户使用。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • Html5-Canvas实现简易的抽奖转盘效果
    本文介绍了如何使用Html5和Canvas标签来实现简易的抽奖转盘效果,同时使用了jQueryRotate.js旋转插件。文章中给出了主要的html和css代码,并展示了实现的基本效果。 ... [详细]
  • 本文介绍了响应式页面的概念和实现方式,包括针对不同终端制作特定页面和制作一个页面适应不同终端的显示。分析了两种实现方式的优缺点,提出了选择方案的建议。同时,对于响应式页面的需求和背景进行了讨论,解释了为什么需要响应式页面。 ... [详细]
  • 本文介绍了如何在Azure应用服务实例上获取.NetCore 3.0+的支持。作者分享了自己在将代码升级为使用.NET Core 3.0时遇到的问题,并提供了解决方法。文章还介绍了在部署过程中使用Kudu构建的方法,并指出了可能出现的错误。此外,还介绍了开发者应用服务计划和免费产品应用服务计划在不同地区的运行情况。最后,文章指出了当前的.NET SDK不支持目标为.NET Core 3.0的问题,并提供了解决方案。 ... [详细]
  • 本文详细介绍了在Centos7上部署安装zabbix5.0的步骤和注意事项,包括准备工作、获取所需的yum源、关闭防火墙和SELINUX等。提供了一步一步的操作指南,帮助读者顺利完成安装过程。 ... [详细]
  • Apache Shiro 身份验证绕过漏洞 (CVE202011989) 详细解析及防范措施
    本文详细解析了Apache Shiro 身份验证绕过漏洞 (CVE202011989) 的原理和影响,并提供了相应的防范措施。Apache Shiro 是一个强大且易用的Java安全框架,常用于执行身份验证、授权、密码和会话管理。在Apache Shiro 1.5.3之前的版本中,与Spring控制器一起使用时,存在特制请求可能导致身份验证绕过的漏洞。本文还介绍了该漏洞的具体细节,并给出了防范该漏洞的建议措施。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • Windows 7 部署工具DISM学习(二)添加补丁的步骤详解
    本文详细介绍了在Windows 7系统中使用部署工具DISM添加补丁的步骤。首先需要将光驱中的安装文件复制到指定文件夹,并进行挂载。然后将需要的MSU补丁解压并集成到系统中。文章给出了具体的命令和操作步骤,帮助读者完成补丁的添加过程。 ... [详细]
  • Windows7 64位系统安装PLSQL Developer的步骤和注意事项
    本文介绍了在Windows7 64位系统上安装PLSQL Developer的步骤和注意事项。首先下载并安装PLSQL Developer,注意不要安装在默认目录下。然后下载Windows 32位的oracle instant client,并解压到指定路径。最后,按照自己的喜好对解压后的文件进行命名和压缩。 ... [详细]
author-avatar
陈可1993_532
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有