views:

831

answers:

2

I am using CWinFormsControl to host a Windows Forms UserControl in an MFC dialog. I have set the property DoubleBufferd to true. According to the docs this results in AllPaintingInWmPaint and UserPaint to be set to true too (not sure if this matters). How can I force (or fake) the UserControl to draw its background transparent?

This is what I have set in the contructor of my UserControl:

this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.BackColor = Color.Transparent;
this.DoubleBuffered = true;
+5  A: 

I have a potential solution that may work, although I would need more information on how your animated controls work to be sure. There is one unfortunate side effect in my solution, and that is that the DoubleBuffering property only works correctly in .NET control containers. When hosted in MFC, your controls will flicker on resize and other similar display-tearing refreshes. This may cause issues with animated controls, depending on how they are performing drawing work.

To start, I first looked for issues when hosting a .NET UserControl in MFC. After quite a while of reading through the instantiation code of CWinFormsControl::CreateControl() and everything beneath, nothing out of the ordinary came up. In fact, aside from the quirks of loading managed references, the code is identical to how transparent ActiveX controls are loaded.

After learning that piece of information, I used Spy++ to look at whether the .NET control is instantiated with a windowed container. Indeed, it is. After a rather lengthy investigation, this control container appears to be controlled by an instance of a utility class, System.Windows.Forms.Control.AxSourcingSite, which has no documentation and almost no visibility. This was a little bit surprising to me, as usually it is the reverse. MFC and the lesser used WTL have great support for in-place activation, and usually controls can work with whatever the host has setup, whether windowed or not.

From here, I checked on whether this same container exists when the .NET control is hosted in a .NET control container. I assumed that perhaps the control would have its own window, without any special adapters. Turns out, I was wrong. The control works the same way as in-place non-windowed controls. This means that in order to preserve behavior, a solution must allow the regular .NET activation to proceed as normal, and when windowed, it should do something else.

A close look at the MFC hosted version reveals an off-white background drawn in by the .NET UserControl. After more spading and testing, this off-white background is definitely drawn in by a hidden layer in the window message handling chain. This means we can hack together a solution by using AllPaintingInWmPaint.

To demonstrate this, here is the source code for a UserControl that can be hosted in both .NET and the MFC managed container. This control relies on the following things to work around the transparency issues.

  1. Add a member variable, m_ReroutePaint, to allow us to know when we need to override the default WM_PAINT behavior.
  2. Override base.CreateParams and add the WS_EX_TRANSPARENT flag. When this property is called, set m_ReroutePaint to true. This property was not called when the Control is activated in a .NET container.
  3. Override the WndProc() method, and patch up WM_PAINT to our liking if we are rerouting painting activities.
  4. Use BeginPaint()/EndPaint() via Interop to setup/teardown WM_PAINT. Use the provided HDC as the initializer for a Graphics object.

Here are some caveats:

  1. The background color of the control cannot be changed through the BackColor .NET property after the control has been instantiated. One can add workarounds for this, but to keep the sample short and simple, I left out code to do this as the intended goal is for transparent controls. However, if you start with a background color that isn't transparent, the workaround is unnecessary. I did leave code in for this case.
  2. In attaching a HDC to a Graphics object in the WM_PAINT handler via Graphics.FromHdc(), the documentation suggests that Graphics.ReleaseHdc() should be called. However, by doing this, a GDI handle leak occurs. I have left it commented out here, but perhaps someone with GDI+ internals knowledge can figure this out.

This UserControl was created in a project named 'UserCtrlLibrary1'. The DebugPrintStyle() items may be safely removed. Also, handlers were added for resize and paint, which are both in a separate designer file, but trivial to add. AllPaintingInWmPaint should be true through the lifetime of the control.

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);
        }
    }
}

In case anyone other than the OP would like to test this, here are the details to get this up and running in MFC. I created a MFC SDI project, without document-view architecture, with ActiveX control support. This results in generation of typical «project-name» class, ChildView class, and MainFrm classes.

Inside the ChildView.h header, add the following header material before the class (but after #pragma once). Alter the name of the .NET control library if yours is different.

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

Add a member variable for the .NET control host. Arbitrarily, I placed mine under the Attributes section.

// Attributes
public:
  CWinFormsControl<CircleControl> m_Circle;

Also, I added handlers for OnCreate() and OnSize(). public/protected visibility may be adjusted as you need.

  // 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);

In ChildView.cpp, I added function bodies for all the items listed above. The message map also needs updates if you didn't use ClassWizard to add the windows message handlers.

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);
}

These changes create an instance of the UserControl, and anchor it against the top half of the view. The OnPaint() handler draws a pink band in the left half of the view. Together, transparency should be apparent in the top left quadrant of the view.

To get the MFC project to compile and run, a copy of the UserCtrlLibrary1 output needs to be placed in the same location as the executables for UserCtrlMFCHost. Also, another copy needs to be placed in the same directory as the project source code files for the #using statement. Last, the MFC project should be modified to use the /clr compilation script. In the Configuration Properties section, General subsection, this switch is listed under Project Defaults.

One interesting thing of note, is that this allows the ^ suffix for access to managed classes. At some points in developing this solution, I debated adding methods to be called only when instantiated from MFC, but given that there are ways to detect windowed/non-windowed activation, this wasn't necessary. Other implementations may need this, though, so I feel it is good to point this out.

How to: Compile MFC and ATL code with /clr

meklarian
A: 

Hi, Meklarian

Great article of using .NET user control in MFC.

Right now, this is exactly the same problem I had. BTW, I implemented User control to MFC. There is a problem is to pass data to user control, also send back data to MFC app.

Really need to figure it out.

Thanks you very much in advance.

Daniel

Daniel Tong