创建带有子窗口(控件)的分层窗口的策略

发布于 2024-12-13 19:17:59 字数 832 浏览 1 评论 0原文

我想创建一个不规则形状/蒙皮的窗口(目前只有圆角、阿尔法混合角)。创建顶级窗口后,我正在处理 WM_CREATE 消息并执行以下操作:

  1. 创建兼容的内存 DC
  2. 创建与窗口 DC 兼容的 DIB 部分
  3. 选择 DIB 到内存 DC
  4. 绘制我的背景
  5. 应用 alpha 通道并预乘 RGB 值

稍后,我计划通过设置 alpha 通道并预乘相关位来实现边缘的舍入,以实现这一点

现在,如果我在窗口中创建一个按钮,它不会自行呈现。我知道为什么,我正在尝试想出一个优雅的解决方案来在这里制作我的自定义控件(子窗口)。

我最初的方法是首先放弃使用子窗口,只让顶层窗口完成所有绘图以及输入处理、命中测试等。我发现这太乏味了,相反我想让 Windows 为我处理这一切。

现在,我知道如果我创建一个子窗口,它当然会表现正常(例如对用户输入做出反应),并且我想利用它。我计划通常使用 CreateWindowEx() 创建子窗口(自定义控件),以便它们获得窗口句柄,并接收窗口消息,而不必担心手动传递它们。

不知何故,我需要绘制这些窗口,正如我所见,唯一可能的方法是从绘制整个顶层窗口的例程中进行。我需要发明某种逻辑,让顶层窗口的绘制例程在必要时绘制我的子窗口。据我了解 UpdateLayeredWindow() 函数需要重绘整个窗口。

例如,让子控件渲染自身的图像并将其发送到顶层窗口的绘制例程是否明智?例如,子窗口将用户 WM 发送到顶级窗口,将指向其渲染位图的指针作为 WPARAM 传递,并将指向定义其位置和大小的结构的指针作为 LPARAM 传递。

还有更好的想法吗?这有任何意义吗?

谢谢, 埃里克

I want to create an irregularily shaped/skinned window (just with rounded, alpha blended corners for now). Upon creating the top-level window i am processing the WM_CREATE message and do the following:

  1. Create a compatible memory DC
  2. Create a DIB section compatible with the window DC
  3. Select DIB into memory DC
  4. Do the drawing of my backdrop
  5. Apply alpha channel and premultiply RGB values
  6. Call UpdateLayeredWindow()

Later on I am planning on rounding of the edges by setting the alpha channel and premultiplying the bits in question to make that happen.

Now, if I create for instance a button in my window, it will not render itself. I know why, and I am trying to come up with an elegant solution to making my custom controls (child windows) here.

My initial approach was to ditch using child windows in the first place and just let the top level window do all the drawing and also input handling, hit testing, and so on. I found this to be way to tedious and instead I want to let Windows handle all this for me.

Now, I know if I create a child window, it of course behaves normally (e.g. reacting to user input), and I want to leverage this. I am planning on creating the child windows (custom controls) normally using CreateWindowEx() so they get a window handle, and recieve window messages without me having to worry about passing them manually.

Somehow I need to get these windows painted, and as I see it, the only possible way to do this is from the routine that paints the whole top level window. I need to invent some kind of logic to get the top level window's paint routine to paint my child windows whenever necessary. As far as I understand the UpdateLayeredWindow() function need to redraw the whole window.

Is it sensible to for instance have the child controls render an image of themselves that are sent to the top level window's paint routine? Like for instance the child window sending a user WM to the top level window passing pointer to its rendered bitmap as a WPARAM and pointer to a structure defining its position and size as a LPARAM.

Any better ideas? Does this make any sense at all?

Thanks,
Eirik

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

祁梦 2024-12-20 19:17:59

我试图做一件非常相似的事情。
通过阅读本文和其他搜索网络。它将绘制 CWnd(或 HWND)及其子级连接到您自己的 CDC(或 HDC)的推荐机制是使用打印 API。

CWnd 有 Print 和 PrintClient 方法,它们可以正确发送 WM_PRINT。还有 Win32 方法:PrintWindow。

一开始我很难让它工作,但我最终得到了正确的方法和参数。对我有用的代码是:

void Vg2pImageHeaderRibbon::Update() {
    // Get dimensions
    CRect window_rect;
    GetWindowRect(&window_rect);

    // Make mem DC + mem  bitmap
    CDC* screen_dc = GetDC(); // Get DC for the hwnd
    CDC dc;
    dc.CreateCompatibleDC(screen_dc);
    CBitmap dc_buffer;
    dc_buffer.CreateCompatibleBitmap(screen_dc, window_rect.Width(), window_rect.Height());
    auto hBmpOld = dc.SelectObject(dc_buffer);

    // Create a buffer for manipulating the raw bitmap pixels (per-pixel alpha).
    // Used by ClearBackgroundAndPrepForPerPixelTransparency and CorrectPerPixelAlpha.
    BITMAP raw_bitmap;
    dc_buffer.GetBitmap(&raw_bitmap);
    int bytes =  raw_bitmap.bmWidthBytes * raw_bitmap.bmHeight;
    std::unique_ptr<char> bits(new char[bytes]);

    // Clears the background black (I want semi-transparent black background).
    ClearBackgroundAndPrepForPerPixelTransparency(dc, raw_bitmap, bytes, bits.get(), dc_buffer);

    // To get the window and it's children to draw using print command
    Print(&dc, PRF_CLIENT | PRF_CHILDREN | PRF_OWNED);

    CorrectPerPixelAlpha(dc, raw_bitmap, bytes, bits.get(), dc_buffer);

    // Call UpdateLayeredWindow
    BLENDFUNCTION blend = {0};
    blend.BlendOp = AC_SRC_OVER;
    blend.SourceConstantAlpha = 255;
    blend.AlphaFormat = AC_SRC_ALPHA;
    CPoint ptSrc;
    UpdateLayeredWindow(
        screen_dc, 
        &window_rect.TopLeft(), 
        &window_rect.Size(), 
        &dc, 
        &ptSrc, 
        0, 
        &blend, 
        ULW_ALPHA
    );

    SelectObject(dc, hBmpOld);
    DeleteObject(dc_buffer);
    ReleaseDC(screen_dc);
}

这对我来说就像是一样。但是,如果您的窗口或子窗口不支持 WM_PRINT,我查看了 CView 类的实现方式,我发现该类提供了一个名为 OnDraw(CDC* dc) 的虚拟方法,该方法提供了一个可供绘制的 DC。 WM_PAINT 的实现方式如下:

CPaintDC dc(this);
OnDraw(&dc);

并且 WM_PAINT 的实现方式如下:

CDC* dc = CDC::FromHandle((HDC)wParam);
OnDraw(dc);

因此 WM_PAINT 和 WM_PRINT 会产生 OnDraw(),并且绘图代码会实现一次。

您基本上可以将相同的逻辑添加到您自己的 CWnd 派生类中。使用 Visual Studio 的类向导可能无法实现这一点。我必须将以下内容添加到消息映射块:

BEGIN_MESSAGE_MAP(MyButton, CButton)
    ...other messages
    ON_MESSAGE(WM_PRINT, OnPrint)
END_MESSAGE_MAP()

和我的处理程序:

LRESULT MyButton::OnPrint(WPARAM wParam, LPARAM lParam) {
    CDC* dc = CDC::FromHandle((HDC)wParam);
    OnDraw(dc);
    return 0;
}

注意:如果您在已经自动支持此功能的类上添加自定义 WM_PRINT 处理程序,那么您将失去默认实现。 OnPrint 没有 CWnd 方法,因此您必须使用 Default() 方法来调用默认处理程序。

我还没有尝试过以下方法,但我希望它有效:

LRESULT MyCWnd::OnPrint(WPARAM wParam, LPARAM lParam) {
    CDC* dc = CDC::FromHandle((HDC)wParam);
    // Do my own drawing using custom drawing
    OnDraw(dc);

    // And get the default handler to draw children
    return Default();
}

上面我定义了一些奇怪的方法:ClearBackgroundAndPrepForPerPixelTransparency 和 CorrectPerPixelAlpha。当子控件完全不透明时,这些允许我将对话框的背景设置为半透明(这是我的每像素透明度)。

// This method is not very efficient but the CBitmap class doens't
// give me a choice I have to copy all the pixel data out, process it and set it back again.
// For performance I recommend using another bitmap class
//
// This method makes every pixel have an opacity of 255 (fully opaque).
void Vg2pImageHeaderRibbon::ClearBackgroundAndPrepForPerPixelTransparency( 
    CDC& dc, const BITMAP& raw_bitmap, int bytes, char* bits, CBitmap& dc_buffer 
) {
    CRect rect;
    GetClientRect(&rect);
    dc.FillSolidRect(0, 0, rect.Width(), rect.Height(), RGB(0,0,0));
    dc_buffer.GetBitmapBits(bytes, bits);
    UINT* pixels = reinterpret_cast<UINT*>(bits);
    for (int c = 0; c < raw_bitmap.bmWidth * raw_bitmap.bmHeight; c++ ){
        pixels[c] |= 0xff000000;
    }
    dc_buffer.SetBitmapBits(bytes, bits);
}

// This method is not very efficient but the CBitmap class doens't
// give me a choice I have to copy all the pixel data out, process it and set it back again.
// For performance I recommend using another bitmap class
//
// This method modifies the opacity value because we know GDI drawing always sets
// the opacity to 0 we find all pixels that have been drawn on since we called 
// For performance I recommend using another bitmap class such as the IcfMfcRasterImage
// ClearBackgroundAndPrepForPerPixelTransparency. Anything that has been drawn on will get an 
// opacity of 255 and all untouched pixels will get an opacity of 100.
void Vg2pImageHeaderRibbon::CorrectPerPixelAlpha( 
    CDC& dc, const BITMAP& raw_bitmap, int bytes, char* bits, CBitmap& dc_buffer 
) {
    const unsigned char AlphaForBackground = 100; // (0 - 255)
    const int AlphaForBackgroundWord = AlphaForBackground << 24;
    dc_buffer.GetBitmapBits(bytes, bits);
    UINT* pixels = reinterpret_cast<UINT*>(bits);
    for (int c = 0; c < raw_bitmap.bmWidth * raw_bitmap.bmHeight; c++ ){
        if ((pixels[c] & 0xff000000) == 0) {
            pixels[c] |= 0xff000000;
        } else {
            pixels[c] = (pixels[c] & 0x00ffffff) | AlphaForBackgroundWord;
        }
    }
    dc_buffer.SetBitmapBits(bytes, bits);
}

这是我的测试应用程序的屏幕截图。当用户将鼠标悬停在“更多按钮”按钮上时,将创建具有半透明背景的对话框。按钮“B1”到“B16”是从 CButton 派生的子控件,并使用上面显示的 Print() 调用进行绘制。您可以在视图的右侧边缘和按钮之间看到半透明背景。

在此处输入图像描述

I was trying to do a very similar thing.
From reading this and other searching web. It seams the recommended mechanism for drawing a CWnd (or HWND) and it's children onto your own CDC (or HDC) is to use the printing API.

CWnd has methods Print and PrintClient and which send WM_PRINT correctly. There is also the Win32 methods: PrintWindow.

I had trouble getting this to work at first but I eventually got the right method and parameters. The code that worked for me was:

void Vg2pImageHeaderRibbon::Update() {
    // Get dimensions
    CRect window_rect;
    GetWindowRect(&window_rect);

    // Make mem DC + mem  bitmap
    CDC* screen_dc = GetDC(); // Get DC for the hwnd
    CDC dc;
    dc.CreateCompatibleDC(screen_dc);
    CBitmap dc_buffer;
    dc_buffer.CreateCompatibleBitmap(screen_dc, window_rect.Width(), window_rect.Height());
    auto hBmpOld = dc.SelectObject(dc_buffer);

    // Create a buffer for manipulating the raw bitmap pixels (per-pixel alpha).
    // Used by ClearBackgroundAndPrepForPerPixelTransparency and CorrectPerPixelAlpha.
    BITMAP raw_bitmap;
    dc_buffer.GetBitmap(&raw_bitmap);
    int bytes =  raw_bitmap.bmWidthBytes * raw_bitmap.bmHeight;
    std::unique_ptr<char> bits(new char[bytes]);

    // Clears the background black (I want semi-transparent black background).
    ClearBackgroundAndPrepForPerPixelTransparency(dc, raw_bitmap, bytes, bits.get(), dc_buffer);

    // To get the window and it's children to draw using print command
    Print(&dc, PRF_CLIENT | PRF_CHILDREN | PRF_OWNED);

    CorrectPerPixelAlpha(dc, raw_bitmap, bytes, bits.get(), dc_buffer);

    // Call UpdateLayeredWindow
    BLENDFUNCTION blend = {0};
    blend.BlendOp = AC_SRC_OVER;
    blend.SourceConstantAlpha = 255;
    blend.AlphaFormat = AC_SRC_ALPHA;
    CPoint ptSrc;
    UpdateLayeredWindow(
        screen_dc, 
        &window_rect.TopLeft(), 
        &window_rect.Size(), 
        &dc, 
        &ptSrc, 
        0, 
        &blend, 
        ULW_ALPHA
    );

    SelectObject(dc, hBmpOld);
    DeleteObject(dc_buffer);
    ReleaseDC(screen_dc);
}

This worked for me just as is. But incase you window or children don't support WM_PRINT I looked at how it was implemented for CView class I discovered that this class provides a virtual method called OnDraw(CDC* dc) that is provided with a DC to draw with. WM_PAINT is implemented something like this:

CPaintDC dc(this);
OnDraw(&dc);

And the WM_PAINT is implemented:

CDC* dc = CDC::FromHandle((HDC)wParam);
OnDraw(dc);

So the WM_PAINT and WM_PRINT results an OnDraw(), and the drawing code implemented once.

You can basically add this same logic your own CWnd derived class. This may not be possible using visual studio's class wizards. I had to add the following to message map block:

BEGIN_MESSAGE_MAP(MyButton, CButton)
    ...other messages
    ON_MESSAGE(WM_PRINT, OnPrint)
END_MESSAGE_MAP()

And my handler:

LRESULT MyButton::OnPrint(WPARAM wParam, LPARAM lParam) {
    CDC* dc = CDC::FromHandle((HDC)wParam);
    OnDraw(dc);
    return 0;
}

NOTE: If you add a custom WM_PRINT handler on a class that already supports this automatically then you loose the default implementation. There isn't a CWnd method for OnPrint so you have to use the Default() method to invoke the default handler.

I have't tried the following but I expect it works:

LRESULT MyCWnd::OnPrint(WPARAM wParam, LPARAM lParam) {
    CDC* dc = CDC::FromHandle((HDC)wParam);
    // Do my own drawing using custom drawing
    OnDraw(dc);

    // And get the default handler to draw children
    return Default();
}

Above I defined some strange methods: ClearBackgroundAndPrepForPerPixelTransparency and CorrectPerPixelAlpha. These allow me to set the background of my dialog be semi-transparent when having the child controls be full opaque (this is my per-pixel transparency).

// This method is not very efficient but the CBitmap class doens't
// give me a choice I have to copy all the pixel data out, process it and set it back again.
// For performance I recommend using another bitmap class
//
// This method makes every pixel have an opacity of 255 (fully opaque).
void Vg2pImageHeaderRibbon::ClearBackgroundAndPrepForPerPixelTransparency( 
    CDC& dc, const BITMAP& raw_bitmap, int bytes, char* bits, CBitmap& dc_buffer 
) {
    CRect rect;
    GetClientRect(&rect);
    dc.FillSolidRect(0, 0, rect.Width(), rect.Height(), RGB(0,0,0));
    dc_buffer.GetBitmapBits(bytes, bits);
    UINT* pixels = reinterpret_cast<UINT*>(bits);
    for (int c = 0; c < raw_bitmap.bmWidth * raw_bitmap.bmHeight; c++ ){
        pixels[c] |= 0xff000000;
    }
    dc_buffer.SetBitmapBits(bytes, bits);
}

// This method is not very efficient but the CBitmap class doens't
// give me a choice I have to copy all the pixel data out, process it and set it back again.
// For performance I recommend using another bitmap class
//
// This method modifies the opacity value because we know GDI drawing always sets
// the opacity to 0 we find all pixels that have been drawn on since we called 
// For performance I recommend using another bitmap class such as the IcfMfcRasterImage
// ClearBackgroundAndPrepForPerPixelTransparency. Anything that has been drawn on will get an 
// opacity of 255 and all untouched pixels will get an opacity of 100.
void Vg2pImageHeaderRibbon::CorrectPerPixelAlpha( 
    CDC& dc, const BITMAP& raw_bitmap, int bytes, char* bits, CBitmap& dc_buffer 
) {
    const unsigned char AlphaForBackground = 100; // (0 - 255)
    const int AlphaForBackgroundWord = AlphaForBackground << 24;
    dc_buffer.GetBitmapBits(bytes, bits);
    UINT* pixels = reinterpret_cast<UINT*>(bits);
    for (int c = 0; c < raw_bitmap.bmWidth * raw_bitmap.bmHeight; c++ ){
        if ((pixels[c] & 0xff000000) == 0) {
            pixels[c] |= 0xff000000;
        } else {
            pixels[c] = (pixels[c] & 0x00ffffff) | AlphaForBackgroundWord;
        }
    }
    dc_buffer.SetBitmapBits(bytes, bits);
}

Here is a screen shot of my test application. When the user hovers the mouse over the "more buttons" button the dialog box is created with a semi-transparent background. The buttons "B1" to "B16" are child controls derived from CButton and are being drawn using the Print() call show above. You can see the semi-transparent background at the right hand edge of the view and between the buttons.

enter image description here

小梨窩很甜 2024-12-20 19:17:59

我想我会采用这个解决方案:

顶级窗口

顶级窗口维护两个位图。一种是显示的窗口,另一种是没有呈现任何子控件的窗口。后者仅在窗口大小改变时才需要重绘。该窗口将有一个消息处理程序,用于在显示的位图上呈现子控件。消息处理程序将期望一个指向包含子控件的 DIB 的指针,或者指向实际位(目前不确定哪个最好)的指针,如 WPARAM,以及一个指向包含子控件的矩形的结构的指针。绘制为 LPARAM。在调用 AlphaBlend() 将子控件位图渲染到显示的位图表面之前,将调用 BitBlt() 来清除底层表面(这是另一个位图出现的地方)。

每当调整大小或由于某种原因需要重绘其子窗口时,父窗口都会调用 EnumChildWindows。当然,这里可能会强制执行某种失效机制,以减少不必要的子控件呈现。但不确定速度的提高是否值得付出努力。

子窗口

创建子控件实例后,将创建与顶级窗口的内部位图兼容的内部位图。子窗口将自己渲染到这个内部位图中,每当需要重绘时,它都会通过 SendMessage() 函数通知其父窗口,传递一个指向其位图的指针作为 WPARAM,并传递一个 RECT 作为定义其位置和尺寸的 LPARAM。如果父窗口需要重绘,它会向其所有子窗口发出一条消息,请求其位图。然后,孩子们将回复与他们决定需要重新绘制自己时通常发送的相同的消息。

埃里克

I think I'm going to go for this solution:

Top level window

The top level window maintains two bitmaps. One which is the displayed window and one without any of the child controls rendered. The latter one will only need redrawing when the window changes size. The window will have a message handler that renders a child control on the displayed bitmap. The message handler will expect a pointer to either a DIB containing the child control, or to the actual bits (not sure which is best at the moment), as the WPARAM, and a pointer to a structure containing the rectangle that the child shall be drawn into as the LPARAM. A call to BitBlt() will be made to clear out the underlying surface (this is where the other bitmap comes in) prior to an AlphaBlend() call for rendering the child control bitmap onto the displayed bitmap surface.

The parent window will call the EnumChildWindows whenever it is resized or for some reason need to redraw its children. There could of course be some kind of invalidation regime enforced here to reduce unnecessary rendering of the child controls. Not sure if the speed increase is worth the effort, though.

Child windows

Upon creation of the child control instance, an internal bitmap compatible with that of the top-level window is created. The child renders itself into this internal bitmap and whenever it needs redrawing it notifies its parent window via the SendMessage() function, passing a pointer to its bitmap as the WPARAM, and a RECT as the LPARAM defining its position and dimensions. If the parent needs redrawing, it issues a message down to all its child windows requesting their bitmap. Childs will then respond with the same message that they normally would send when they decide they need redrawing themselves.

Eirik

你在看孤独的风景 2024-12-20 19:17:59

引用WM_PAINT 的 MSDN 页面

WM_PAINT 消息由系统生成,不应由应用程序发送。要强制窗口绘制到特定的设备上下文中,请使用 WM_PRINT 或 WM_PRINTCLIENT 消息。请注意,这需要目标窗口支持 WM_PRINTCLIENT 消息。大多数常见控件都支持 WM_PRINTCLIENT 消息。

因此,您似乎可以使用 EnumChildWindows 迭代所有子窗口,并且使用您的内存 DC 向它们发送 WM_PRINT在步骤 5 和 6 之间。

它看起来像这样:

static BOOL CALLBACK MyPaintCallback(HWND hChild,LPARAM lParam) {
    SendMessage(hChild,WM_PRINT,(WPARAM)lParam,(LPARAM)(PRF_CHECKVISIBLE|PRF_CHILDREN|PRF_CLIENT));
    return TRUE;
}

void MyPaintMethod(HWND hWnd) {
    //steps 1-5

    EnumChildWindows(hWnd,MyPaintCallback,MyMemoryDC);

    //step 6
}

To quote the MSDN page for WM_PAINT:

The WM_PAINT message is generated by the system and should not be sent by an application. To force a window to draw into a specific device context, use the WM_PRINT or WM_PRINTCLIENT message. Note that this requires the target window to support the WM_PRINTCLIENT message. Most common controls support the WM_PRINTCLIENT message.

So it looks like you can iterate through all the child windows with EnumChildWindows and send them WM_PRINT with your memory DC between steps 5 and 6.

It will look something like this:

static BOOL CALLBACK MyPaintCallback(HWND hChild,LPARAM lParam) {
    SendMessage(hChild,WM_PRINT,(WPARAM)lParam,(LPARAM)(PRF_CHECKVISIBLE|PRF_CHILDREN|PRF_CLIENT));
    return TRUE;
}

void MyPaintMethod(HWND hWnd) {
    //steps 1-5

    EnumChildWindows(hWnd,MyPaintCallback,MyMemoryDC);

    //step 6
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文