【问题标题】:Strategy for creating a layered window with child windows (controls)使用子窗口(控件)创建分层窗口的策略
【发布时间】:2011-11-02 13:40:47
【问题描述】:

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

  1. 创建兼容的内存 DC
  2. 创建与窗口 DC 兼容的 DIB 部分
  3. 选择 DIB 到内存 DC
  4. 画我的背景
  5. 应用 Alpha 通道并预乘 RGB 值
  6. 调用 UpdateLayeredWindow()

稍后我计划通过设置 Alpha 通道并预乘相关位来实现边缘的舍入。

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

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

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

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

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

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

谢谢, 埃里克

【问题讨论】:

  • 这里最简单的做法可能是使用 SetLayeredWindowAttributes 方法而不是 UpdateLayeredWindow;您将失去每像素的 alpha,但该窗口将像带有子窗口的普通 Win32 窗口一样工作。 (使用您的位图作为父窗口的背景,并使用颜色键获得不规则的形状)。自己组合子窗口的诀窍在于,您必须“不断地”执行此操作,以便即使用户与它们进行交互(例如,将字符输入到具有闪烁插入符号的编辑中),它们也能保持更新。跨度>
  • 嗨!是的,SetLayeredWindowAttributes 路由可能是一个选项。我以前用过它来使整个窗口透明。我当然同意这会大大简化事情,但是当失去每像素 alpha 时,我也会失去我所追求的漂亮混合效果,对吧?还是我误解了您使用的颜色键术语?我知道,为了在用户交互时更新控件,需要做很多家务。闪烁的插入符号就是一个很好的例子......
  • 是的,您会丢失每像素的 Alpha,因此您无法获得平滑的混合。颜色键是允许您拥有不规则形状的原因 - 您选择一种被视为透明的颜色,因此窗口上以该颜色作为背景的区域是透明的。但这是二元的,开或关,不会混合。

标签: c++ winapi alpha layered


【解决方案1】:

我想我会选择这个解决方案:

顶层窗口

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

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

子窗口

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

埃里克

【讨论】:

    【解决方案2】:

    我试图做一件非常相似的事情。 从阅读这个和其他搜索网络。它将绘制 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() 调用进行绘制。您可以在视图的右侧边缘和按钮之间看到半透明背景。

    【讨论】:

      【解决方案3】:

      引用MSDN page for WM_PAINT

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

      所以看起来您可以使用EnumChildWindows 遍历所有子窗口,并在第 5 步和第 6 步之间使用您的内存 DC 向它们发送 WM_PRINT

      它看起来像这样:

      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
      }
      

      【讨论】:

      • 您好,感谢您的回答。我确实尝试过,但是当使用 WM_PRINT 和 WM_PRINTCLIENT 消息时,我最终在窗口的左上角绘制了控件。当调整窗口大小时,它(整个窗口)不断地来回跳跃到屏幕上的 0,0 位置以及调整大小之前的原始位置。不知道为什么会这样,需要一些调查,但也许我可以调整这种方法来满足我的需要。
      • 在调整大小期间是否通过将窗口自身重新定位到 0,0 来解决这个问题。与我将 POINT {0,0} 而不是 NULL 传递给指定窗口位置的 UpdateLayeredWindow 函数的参数有关......有点白痴错误。
      猜你喜欢
      • 1970-01-01
      • 2010-10-21
      • 2011-06-21
      • 1970-01-01
      • 2018-07-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多