【问题标题】:Transparent background for MFC-hosted Windows Forms UserControlMFC 托管的 Windows 窗体 UserControl 的透明背景
【发布时间】:2009-12-07 06:30:30
【问题描述】:

我正在使用 CWinFormsControl 在 MFC 对话框中托管 Windows 窗体用户控件。我已将属性 DoubleBufferd 设置为 true。根据文档,这导致AllPaintingInWmPaintUserPaint 也设置为true(不确定这是否重要)。如何强制(或伪造)UserControl 将其背景绘制为透明?

这是我在 UserControl 的构造函数中设置的:

this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.BackColor = Color.Transparent;
this.DoubleBuffered = true;

【问题讨论】:

  • 您有什么基本要求需要在用户控件上设置透明背景?
  • @Aidan 我们有一个基于 MFC 的框架,可以在用户配置的对话框上显示各种控件。它们中的大多数是经典的 MFC 控件,但用户也可以编写自定义 Winforms UserControls,这包括需要双缓冲的动画内容。所有这些控件可能会重叠,为什么我们至少需要 UserControl 的透明度。例如,用户将背景设置为图像或其他控件,并希望在其之上有一个透明的 UserControl。

标签: mfc interop transparency winforms


【解决方案1】:

我有一个可行的解决方案,但我需要更多关于动画控件如何工作的信息才能确定。我的解决方案有一个不幸的副作用,那就是 DoubleBuffering 属性只能在 .NET 控件容器中正常工作。在 MFC 中托管时,您的控件将在调整大小和其他类似的显示撕裂刷新时闪烁。这可能会导致动画控件出现问题,具体取决于它们执行绘图工作的方式。

首先,我首先在 MFC 中托管 .NET UserControl 时查找问题。在阅读了 CWinFormsControl::CreateControl() 的实例化代码和下面的所有内容之后,没有出现任何异常。事实上,除了加载托管引用的怪癖之外,代码与加载透明 ActiveX 控件的方式相同。

在了解了那条信息后,我使用 Spy++ 来查看 .NET 控件是否使用窗口容器进行实例化。它的确是。经过相当长时间的调查后,这个控件容器似乎是由实用程序类 System.Windows.Forms.Control.AxSourcingSite 的一个实例控制的,它没有文档,也几乎没有可见性。这让我有点惊讶,因为通常情况正好相反。 MFC 和较少使用的 WTL 对就地激活有很好的支持,并且通常控件可以与主机设置的任何内容一起使用,无论是否窗口化。

从这里,我检查了当 .NET 控件托管在 .NET 控件容器中时是否存在相同的容器。我假设控件可能有自己的窗口,没有任何特殊的适配器。原来,我错了。该控件的工作方式与就地非窗口控件相同。这意味着,为了保持行为,解决方案必须允许常规 .NET 激活正常进行,并且在窗口化时,它应该执行其他操作。

仔细查看 MFC 托管版本会发现由 .NET UserControl 绘制的灰白色背景。经过更多的打磨和测试,这个灰白色的背景肯定是由窗口消息处理链中的隐藏层绘制的。这意味着我们可以使用 AllPaintingInWmPaint 组合出一个解决方案。

为了证明这一点,这里是一个 UserControl 的源代码,它可以托管在 .NET 和 MFC 托管容器中。此控件依赖于以下内容来解决透明度问题。

  1. 添加一个成员变量 m_ReroutePaint,让我们知道何时需要覆盖默认的 WM_PAINT 行为。
  2. 覆盖 base.CreateParams 并添加 WS_EX_TRANSPARENT 标志。调用此属性时,将 m_ReroutePaint 设置为 true。在 .NET 容器中激活控件时未调用此属性。
  3. 重写 WndProc() 方法,如果我们要重新路由绘画活动,则根据我们的喜好修补 WM_PAINT。
  4. 通过 Interop 使用 BeginPaint()/EndPaint() 来设置/拆卸 WM_PAINT。使用提供的 HDC 作为 Graphics 对象的初始化程序。

以下是一些注意事项:

  1. 在控件实例化后,无法通过 BackColor .NET 属性更改控件的背景颜色。可以为此添加解决方法,但为了保持示例简短和简单,我省略了执行此操作的代码,因为预期目标是透明控件。但是,如果您从不透明的背景颜色开始,则不需要解决方法。我确实为这个案例留下了代码。
  2. 在通过 Graphics.FromHdc() 将 HDC 附加到 WM_PAINT 处理程序中的 Graphics 对象时,文档建议应该调用 Graphics.ReleaseHdc()。但是,这样做会发生 GDI 句柄泄漏。我已经把它留在这里注释掉了,但也许有 GDI+ 内部知识的人可以解决这个问题。

此用户控件是在名为“UserCtrlLibrary1”的项目中创建的。可以安全地删除 DebugPrintStyle() 项。此外,还为调整大小和绘制添加了处理程序,它们都在单独的设计器文件中,但添加起来很简单。 AllPaintingInWmPaint 在控件的整个生命周期内都应该为真。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

using System.Runtime.InteropServices;
using System.Diagnostics;

namespace UserCtrlLibrary1
{
    public partial class CircleControl : UserControl
    {
        public CircleControl()
        {
            InitializeComponent();

            DebugPrintStyle(ControlStyles.SupportsTransparentBackColor, "initial");
            DebugPrintStyle(ControlStyles.AllPaintingInWmPaint, "initial");
            DebugPrintStyle(ControlStyles.UserPaint, "initial");

            this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
            this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            this.SetStyle(ControlStyles.UserPaint, true);

            DebugPrintStyle(ControlStyles.SupportsTransparentBackColor, "current");
            DebugPrintStyle(ControlStyles.AllPaintingInWmPaint, "current");
            DebugPrintStyle(ControlStyles.UserPaint, "current");
        }

        public void DebugPrintStyle(ControlStyles cs, string prefix)
        {
            Debug.Print("{0}: {1}={2}", prefix, cs.ToString(), this.GetStyle(cs).ToString());
        }

        bool m_ReroutePaint;
        const int WS_EX_TRANSPARENT = 0x0020;
        protected override CreateParams CreateParams
        {
            get
            {
                if (this.BackColor == Color.Transparent)
                {
                    m_ReroutePaint = true;
                    CreateParams cp = base.CreateParams;
                    cp.ExStyle |= WS_EX_TRANSPARENT;
                    return cp;
                }
                else
                {
                    return base.CreateParams;
                }
            }
        }

        private void CircleControl_Paint(object sender, PaintEventArgs e)
        {
            Graphics g = e.Graphics;

            using (SolidBrush b = new SolidBrush(Color.Orange))
            {
                g.FillEllipse(b, 0, 0, this.Width, this.Height);
            }
        }

        private void CircleControl_Resize(object sender, EventArgs e)
        {
            this.Invalidate();
        }

        const int WM_PAINT = 0x000F;
        [DllImport("user32.dll")]
        static extern IntPtr BeginPaint(IntPtr hwnd, out PAINTSTRUCT lpPaint);
        [DllImport("user32.dll")]
        static extern bool EndPaint(IntPtr hWnd, [In] ref PAINTSTRUCT lpPaint);
        [Serializable, StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
        }
        [StructLayout(LayoutKind.Sequential)]
        struct PAINTSTRUCT
        {
            public IntPtr hdc;
            public bool fErase;
            public RECT rcPaint;
            public bool fRestore;
            public bool fIncUpdate;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
            public byte[] rgbReserved;
        }

        protected override void WndProc(ref Message m)
        {
            if ((m.Msg == WM_PAINT) && (m_ReroutePaint))
            {
                PAINTSTRUCT ps = new PAINTSTRUCT();
                BeginPaint(this.Handle, out ps);
                using (Graphics g = Graphics.FromHdc(ps.hdc))
                {
                    using (PaintEventArgs e = new PaintEventArgs(g, new Rectangle(ps.rcPaint.Left, ps.rcPaint.Top, ps.rcPaint.Right - ps.rcPaint.Left, ps.rcPaint.Bottom - ps.rcPaint.Top)))
                    {
                        this.OnPaint(e);
                    }
                    // HACK: This is supposed to be required...
                    //       but it leaks handles when called!
                    //g.ReleaseHdc(ps.hdc);
                }
                EndPaint(this.Handle, ref ps);
                return;
            }

            base.WndProc(ref m);
        }
    }
}

如果 OP 以外的任何人想要对此进行测试,以下是在 MFC 中启动和运行它的详细信息。我创建了一个 MFC SDI 项目,没有文档视图架构,支持 ActiveX 控件。这会生成典型的 «project-name» 类、ChildView 类和 MainFrm 类。

在 ChildView.h 标头中,在类之前添加以下标头材料(但在 #pragma once 之后)。如果您的不同,请更改 .NET 控件库的名称。

#include <afxwinforms.h>
#using "UserCtrlLibrary1.dll"
using namespace UserCtrlLibrary1;

为 .NET 控制主机添加一个成员变量。随意,我把我的放在属性部分下。

// Attributes
public:
  CWinFormsControl<CircleControl> m_Circle;

另外,我为 OnCreate() 和 OnSize() 添加了处理程序。可以根据需要调整公共/受保护的可见性。

  // Generated message map functions
protected:
  afx_msg void OnPaint();
  DECLARE_MESSAGE_MAP()
public:
  afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
  afx_msg void OnSize(UINT nType, int cx, int cy);

在 ChildView.cpp 中,我为上面列出的所有项目添加了函数体。如果您没有使用 ClassWizard 添加 Windows 消息处理程序,消息映射也需要更新。

BEGIN_MESSAGE_MAP(CChildView, CWnd)
  ON_WM_PAINT()
  ON_WM_CREATE()
  ON_WM_SIZE()
END_MESSAGE_MAP()

void CChildView::OnPaint() 
{
  CPaintDC dc(this); // device context for painting

  RECT rt;
  this->GetClientRect(&rt);

  rt.right = (rt.right + rt.left)/2;
  dc.FillSolidRect(&rt, RGB(0xFF, 0xA0, 0xA0));
}

int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
  if (CWnd::OnCreate(lpCreateStruct) == -1)
    return -1;

  RECT rt;
  this->GetClientRect(&rt);
  m_Circle.CreateManagedControl(WS_VISIBLE, rt, this, 1);

  return 0;
}

void CChildView::OnSize(UINT nType, int cx, int cy)
{
  CWnd::OnSize(nType, cx, cy);

  RECT rt;
  this->GetClientRect(&rt);
  m_Circle.MoveWindow(rt.left, rt.top, rt.right - rt.left, (rt.bottom - rt.top)/2, TRUE);
}

这些更改创建了 UserControl 的一个实例,并将其锚定在视图的上半部分。 OnPaint() 处理程序在视图的左半边绘制一条粉色带。总之,透明度应该在视图的左上象限中很明显。

要编译和运行 MFC 项目,需要将 UserCtrlLibrary1 输出的副本放置在与 UserCtrlMFCHost 的可执行文件相同的位置。此外,需要将另一个副本放在与#using 语句的项目源代码文件相同的目录中。最后,应修改 MFC 项目以使用 /clr 编译脚本。在 Configuration Properties 部分的 General 子部分中,此开关列在 Project Defaults 下。

值得注意的一件有趣的事情是,这允许使用 ^ 后缀来访问托管类。在开发此解决方案的某些时候,我讨论了添加仅在从 MFC 实例化时调用的方法,但鉴于有检测窗口/非窗口激活的方法,这不是必需的。不过,其他实现可能需要这个,所以我觉得指出这一点很好。

How to: Compile MFC and ATL code with /clr

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-05-20
    • 2016-02-05
    • 2011-05-06
    • 2012-12-27
    • 2016-03-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多