简体   繁体   English

使用子窗口创建分层窗口的策略(控件)

[英]Strategy for creating a layered window with child windows (controls)

I want to create an irregularily shaped/skinned window (just with rounded, alpha blended corners for now). 我想创建一个不规则形状/皮肤的窗口(现在只有圆形,alpha混合角)。 Upon creating the top-level window i am processing the WM_CREATE message and do the following: 创建顶级窗口后,我正在处理WM_CREATE消息并执行以下操作:

  1. Create a compatible memory DC 创建兼容的内存DC
  2. Create a DIB section compatible with the window DC 创建与窗口DC兼容的DIB部分
  3. Select DIB into memory DC 选择DIB到内存DC
  4. Do the drawing of my backdrop 做我的背景画
  5. Apply alpha channel and premultiply RGB values 应用Alpha通道和预乘RGB值
  6. Call UpdateLayeredWindow() 调用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. 后来,我计划通过设置alpha通道并预先计算相关位来对边缘进行舍入,以实现这一点。

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. 我发现这是单调乏味的方式,而我想让Windows为我处理这一切。

Now, I know if I create a child window, it of course behaves normally (eg 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. 我正计划通常使用CreateWindowEx()来创建子窗口(自定义控件),这样他们就可以获得窗口句柄,并接收窗口消息,而不必担心手动传递它们。

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. 据我所知,UpdateLayeredWindow()函数需要重绘整个窗口。

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. 例如,子窗口将用户WM发送到顶级窗口,将指向其呈现的位图的指针作为WPARAM传递,并将指针定义为将其位置和大小定义为LPARAM的结构。

Any better ideas? 有更好的想法吗? Does this make any sense at all? 这有什么意义吗?

Thanks, Eirik 谢谢,Eirik

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(或HWND),并且它的子项到您自己的CDC(或HDC)上是使用打印API。

CWnd has methods Print and PrintClient and which send WM_PRINT correctly. CWnd具有方法Print和PrintClient,并且正确地发送WM_PRINT。 There is also the Win32 methods: PrintWindow. 还有Win32方法: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_PRINT我看了它是如何为CView类实现的我发现这个类提供了一个名为OnDraw(CDC * dc)的虚拟方法,它提供了一个DC来绘制。 WM_PAINT is implemented something like this: WM_PAINT实现如下:

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

And the WM_PAINT is implemented: 并且实现了WM_PAINT:

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

So the WM_PAINT and WM_PRINT results an OnDraw(), and the drawing code implemented once. 因此,WM_PAINT和WM_PRINT产生一个OnDraw(),并且绘图代码实现了一次。

You can basically add this same logic your own CWnd derived class. 您基本上可以将自己的CWnd派生类添加到相同的逻辑中。 This may not be possible using visual studio's class wizards. 使用visual studio的类向导可能无法实现这一点。 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. 注意:如果在已自动支持此类的类上添加自定义WM_PRINT处理程序,则会松开默认实现。 There isn't a CWnd method for OnPrint so you have to use the Default() method to invoke the default handler. OnPrint没有CWnd方法,因此您必须使用Default()方法来调用默认处理程序。

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. 上面我定义了一些奇怪的方法:ClearBackgroundAndPrepForPerPixelTransparency和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. 按钮“B1”到“B16”是从CButton派生的子控件,并使用上面的Print()调用绘制。 You can see the semi-transparent background at the right hand edge of the view and between the buttons. 您可以在视图的右侧边缘和按钮之间看到半透明背景。

在此输入图像描述

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. 消息处理程序将指向一个指向包含子控件的DIB或指向实际位(不确定哪个是最好的)作为WPARAM的指针,以及一个指向包含该子项应该为矩形的结构的指针。作为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. 在AlphaBlend()调用之前,将调用BitBlt()以清除底层表面(这是其他位图的位置),以便将子控件位图渲染到显示的位图表面上。

The parent window will call the EnumChildWindows whenever it is resized or for some reason need to redraw its children. 只要调整大小或由于某种原因需要重绘其子项,父窗口将调用EnumChildWindows。 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. 子将其自身渲染到此内部位图中,每当需要重绘时,它通过SendMessage()函数通知其父窗口,将指针作为WPARAM传递给其位图,并将RECT作为LPARAM定义其位置和尺寸。 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 的Eirik

To quote the MSDN page for WM_PAINT : 引用WM_PAINTMSDN页面

The WM_PAINT message is generated by the system and should not be sent by an application. WM_PAINT消息由系统生成,不应由应用程序发送。 To force a window to draw into a specific device context, use the WM_PRINT or WM_PRINTCLIENT message. 要强制窗口绘制到特定设备上下文,请使用WM_PRINT或WM_PRINTCLIENT消息。 Note that this requires the target window to support the WM_PRINTCLIENT message. 请注意,这需要目标窗口支持WM_PRINTCLIENT消息。 Most common controls support the WM_PRINTCLIENT message. 最常见的控件支持WM_PRINTCLIENT消息。

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. 因此,您可以使用EnumChildWindows遍历所有子窗口,并在步骤5和步骤6之间将内存DC发送给WM_PRINT

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
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM