我有一个可行的解决方案,但我需要更多关于动画控件如何工作的信息才能确定。我的解决方案有一个不幸的副作用,那就是 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 托管容器中。此控件依赖于以下内容来解决透明度问题。
- 添加一个成员变量 m_ReroutePaint,让我们知道何时需要覆盖默认的 WM_PAINT 行为。
- 覆盖 base.CreateParams 并添加 WS_EX_TRANSPARENT 标志。调用此属性时,将 m_ReroutePaint 设置为 true。在 .NET 容器中激活控件时未调用此属性。
- 重写 WndProc() 方法,如果我们要重新路由绘画活动,则根据我们的喜好修补 WM_PAINT。
- 通过 Interop 使用 BeginPaint()/EndPaint() 来设置/拆卸 WM_PAINT。使用提供的 HDC 作为 Graphics 对象的初始化程序。
以下是一些注意事项:
- 在控件实例化后,无法通过 BackColor .NET 属性更改控件的背景颜色。可以为此添加解决方法,但为了保持示例简短和简单,我省略了执行此操作的代码,因为预期目标是透明控件。但是,如果您从不透明的背景颜色开始,则不需要解决方法。我确实为这个案例留下了代码。
- 在通过 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