Opened 2 years ago

Closed 14 months ago

Last modified 14 months ago

#14492 closed enhancement (wontfix)

Fix transparency issues for static text and check box under Windows

Reported by: wxBen Owned by:
Priority: normal Milestone: 3.0.0
Component: wxMSW Version: stable-latest
Keywords: transparent checkbox static text Cc:
Blocked By: Blocking:
Patch: yes

Description

Under Microsoft Windows, wxStaticText and wxCheckBox are typically only transparent in so much as their backgrounds are painted with the background colour of a parent. This is nice and usually works well, but if you place one over say a ribbon bar which has a background gradient, then you need true transparency.

If you Google this, you will see other people struggling with this as well. The wxWidgets forums contains code for TransparentStaticText class. Also, there is a Python tutorial about writing your own check box class in order to have a transparent one with numerous references to it. See here for example: http://social.msdn.microsoft.com/Forums/eu/vcgeneral/thread/a0db67c8-3b51-4451-a636-a2737e6b3e27 and see the screenshot.

Why does this not work out of the box? I do not want to add all sorts of extra classes and things to my application. I have been stepping through the code and trying things like:

text->SetBackgroundColour(wxTransparentColour);
text->SetBackgroundStyle(wxBG_STYLE_TRANSPARENT);

But to no avail. The core issues seem to be this:

  • True transparency requires an extended Windows style of WS_EX_TRANSPARENT. Without this, it will not work.
  • A window with this style should not paint the background in response to WM_ERASEBKGND and indicate that it has handled the message.
  • For a child control the parent should, in response to messages like WM_CTLCOLORSTATIC, set the background mode to transparent and return a handle to the HOLLOW_BRUSH. See here for an example:

http://social.msdn.microsoft.com/Forums/eu/vcgeneral/thread/a0db67c8-3b51-4451-a636-a2737e6b3e27

So my code changes are this:

  • Fixed the wxControl class to detect the WS_EX_TRANSPARENT style and do the proper thing in general in response to messages like WM_CTLCOLORSTATIC. This makes transparent static texts work.
  • Fixed wxCheckBox which was always passing in zero for the extended style and ignoring any wxTRANSPARENT_WINDOW passed in. You absolutely need it here and other controls pass it on to the window creation code. Why is the checkbox different? In the case of the checkbox, also switch to owner draw, as it does not always work otherwise.
  • Added some comments to the documentation to help people down the line.

I also update the ribbon bar sample case and added a static text and checkbox onto it. Note how the ribbon background comes through, but if you remove WS_EX_TRANSPARENT or any of the code changes, the backgrounds revert to white.

These changes only come into play when a non default wxTRANSPARENT_WINDOW is passed when creating these controls. This style is also not demonstrated in any of the samples or used much in the code, so the impact of the changes should be low. The changes are also consistent with recommendations from the MSDN forums about how this is supposed to be done. Also, our application has a lot of native transparent controls, so I have some experience with this.

While I did not test this for radio buttons and such, it may work for them as well, and if needed only a minor code tweak should be necessary to get those to work as well.

Attachments (1)

TransCheckBoxStaticText.diff download (2.2 KB) - added by wxBen 2 years ago.
TransCheckBoxStaticText.diff

Download all attachments as: .zip

Change History (24)

comment:2 Changed 2 years ago by wxBen

  • Patch set
  • Type changed from defect to enhancement

comment:3 Changed 2 years ago by wxBen

There seems to be a general need for this patch. Look in the forums how many comment on not having true transparency. Here is another one which asks about this patch and if it would also work from wxRadioButton:

http://forums.wxwidgets.org/viewtopic.php?f=1&t=35527&p=145448#p145448

comment:4 follow-up: Changed 2 years ago by wxBen

Someone tried this patch for the radio button, and added this:

Your patch worked very well. For wxRadioButton only following code I added in src/msw/radiobut.cpp file

WXDWORD exStyle = 0; 
if (style & wxTRANSPARENT_WINDOW) 
{ 
       exStyle |= WS_EX_TRANSPARENT; 
} 

bool ret = MSWCreateControl(wxT("BUTTON"), msStyle, pos, size, label, exStyle); 

return ret;

Maybe the person who integrates/tests/submits this patch, can add the above code change as well, please?

comment:5 follow-up: Changed 2 years ago by vadz

  • Status changed from new to infoneeded_new

Sorry, I'm not sure which problem exactly does this solve. I thought using wxStaticText and wxCheckBox on themed backgrounds already did work in wxMSW, when doesn't it work exactly?

Also, telling people to use wxTRANSPARENT_WINDOW for their controls is not perfect as things work fine without it in non-MSW ports. So perhaps we should use it by default? The main problem I see is that this makes wxCheckBox (and wxRadioButton too I'd guess as they're the same thing internally in MSW) always owner-drawn. Why is this necessary exactly?

comment:6 in reply to: ↑ 5 Changed 2 years ago by robind

Replying to vadz:

Sorry, I'm not sure which problem exactly does this solve. I thought using wxStaticText and wxCheckBox on themed backgrounds already did work in wxMSW, when doesn't it work exactly?

I don't think themed backgrounds are the problem, usually questions or issues like this are raised when somebody wants to put a put a control on a custom background, such as a custom drawn gradient or an image background.

comment:7 Changed 2 years ago by vadz

Do we have some simple test case showing what exactly doesn't work? We do have something in the erase sample but it does work there...

comment:8 Changed 2 years ago by wxBen

  • Status changed from infoneeded_new to new

As for showing exactly what this fixes, apply the patch and then change the two lines in the patch in RibbonDemo.cpp from:

wxCheckBox* checkBox = new wxCheckBox(panel, wxID_ANY, wxT("Checkbox"), wxDefaultPosition, wxDefaultSize, wxTRANSPARENT_WINDOW); 
wxStaticText* text = new wxStaticText(panel, wxID_ANY, wxT("StaticText"), wxDefaultPosition, wxDefaultSize, wxTRANSPARENT_WINDOW); 

to

wxCheckBox* checkBox = new wxCheckBox(panel, wxID_ANY, wxT("Checkbox"));
wxStaticText* text = new wxStaticText(panel, wxID_ANY, wxT("StaticText"));

and revert the other changes. Now compile and run the demo and look on the transparency panel. Note how the checkbox and statictext have white backgrounds. There is a ribbon bar gradient behind them that should be showing through.

You can see another example of the same sort of problem here: http://img703.imageshack.us/img703/372/backgrounddfi.png
(which is discussed here: http://comments.gmane.org/gmane.comp.python.wxpython/88453)

As robind says: "I don't think themed backgrounds are the problem, usually questions or issues like this are raised when somebody wants to put a put a control on a custom background, such as a custom drawn gradient or an image background."

So the problem happens when you want to put a transparent control over something that draws a gradient for background rather than use one solid colour (otherwise it would work).

Making windows transparent under MS windows requires the WS_EX_TRANSPARENT flag when they are created. If you Google that and HOLLOW_BRUSH you will see numerous references to that. No way around that.

Yes, wxWidgets does have transparent widgets, but they are not really transparent, they only look transparent because they pick up the solid background colour from the parent and use that. But when that parent has no solid background colour or has a gradient like the ribbon bar does, then it does not work, and then the only way for it to work is with the fix contained in this patch.

Just search for transparent in the forum and you will see how many people run into this. See these for example:
http://forums.wxwidgets.org/viewtopic.php?f=1&t=35527
http://forums.wxwidgets.org/viewtopic.php?f=1&t=35328&p=144566&hilit=transparent#p144566
http://forums.wxwidgets.org/viewtopic.php?f=1&t=29913&hilit=text+background+transparent
http://forums.wxwidgets.org/viewtopic.php?f=1&t=35074&p=144417&hilit=transparent#p144417

I would not always use WS_EX_TRANSPARENT, because doing so does cause unnecessary repaints of background underneath controls that are not really transparent. If the flag is not used, then MS Windows clips the areas for child controls. If the flag is used, then the area for such children is not clipped, which means the parent window gets to actually paint that background. But that cost is likely minor.

comment:9 Changed 2 years ago by wxBen

Remember, the changes in the patch only take effect when the non default and rarely used flag wxTRANSPARENT_WINDOW is used. Otherwise nobody is impacted. And the changes are consistent with what you find in a lot of MS documentation about transparent controls. So I think the risk of this patch is low. Yet the issue and benefits are clear.

comment:10 in reply to: ↑ 4 Changed 2 years ago by rakeshthp

Replying to wxBen:

Someone tried this patch for the radio button, and added this:

Your patch worked very well. For wxRadioButton only following code I added in src/msw/radiobut.cpp file

WXDWORD exStyle = 0; 
if (style & wxTRANSPARENT_WINDOW) 
{ 
       exStyle |= WS_EX_TRANSPARENT; 
} 

bool ret = MSWCreateControl(wxT("BUTTON"), msStyle, pos, size, label, exStyle); 

return ret;

Maybe the person who integrates/tests/submits this patch, can add the above code change as well, please?

Hi wxBen,

That's me who added that comment. Well, there seems to be a small problem here in windows with that code. I am working on windows XP. Presently my windows appearance is windows classic. With this setting, there is no issue with background transparency of wxRadioButton. As soon as I change this setting to default Windows XP Style, it shows a black rectangle only for the text part and no text is visible. Again when appearance is changed to windows classic style, it shows up properly.

Thanks and Regards

comment:11 follow-up: Changed 2 years ago by vadz

@wxBen: wxMSW implements transparency by asking the parent to draw its children background. This can, in principle, work for gradient backgrounds just fine. I didn't have time to look at what goes on in wxRibbon case but transparent controls do (mostly) work for wxToolBar (although see this thread) so there is no reason they couldn't work for our own classes too, it should actually be simpler as at least our code is the same for all Windows versions unlike the toolbar control.

The trouble with using wxTRANSPARENT_WINDOW is that it risks introducing new bugs (and from the comment above, it looks like we didn't need to wait too long for this to happen) and, more importantly, that it shouldn't be needed. As the code works fine without it in wxGTK, it should also work in wxMSW. Of course, if we can't make it work, we could consider using this style as a workaround but I just don't understand why can't we use the parent-paints-background-approach here too, it looks like it ought to work.

comment:12 in reply to: ↑ 11 Changed 2 years ago by rakeshthp

Replying to vadz:

@wxBen: wxMSW implements transparency by asking the parent to draw its children background. This can, in principle, work for gradient backgrounds just fine. I didn't have time to look at what goes on in wxRibbon case but transparent controls do (mostly) work for wxToolBar (although see this thread) so there is no reason they couldn't work for our own classes too, it should actually be simpler as at least our code is the same for all Windows versions unlike the toolbar control.

The trouble with using wxTRANSPARENT_WINDOW is that it risks introducing new bugs (and from the comment above, it looks like we didn't need to wait too long for this to happen) and, more importantly, that it shouldn't be needed. As the code works fine without it in wxGTK, it should also work in wxMSW. Of course, if we can't make it work, we could consider using this style as a workaround but I just don't understand why can't we use the parent-paints-background-approach here too, it looks like it ought to work.

Well, that's the question vadz. why the technique you are telling (I just don't understand why can't we use the parent-paints-background-approach here too, it looks like it ought to work) isn't giving proper result? If it would have worked, there wouldn't had been so much discussion around. :-) Well, I just hope things will get sorted out slowly in the due course of time. :)

comment:13 Changed 2 years ago by wxBen

The current wxWidget code grabs the parent background colour and returns a solid brush for that colour. This is what happens:

  1. wxWidgets creates the child controls and they are default opaque.
  2. The platform paints the parent window background, and will typically clip the child control areas and not paint under them.
  3. The child control needs to be painted and the platform asks the parent for a background brush. wxWidgets returns a solid brush corresponding the parent background colour, which the platform uses to paint the control background before the platform paints the control itself.

This works well, except when the parent does not have on solid background colour. The effect you get then is shown by the screen shot URL.

A more general approach would be this:

  1. wxWidgets creates the child controls and the existing platform specific code now also makes the window transparent. How this is done depends on the platform. Under Windows it is WS_EX_TRANSPARENT as in this patch, under GTK you change the alpha channel transparency of the child window to full (using gtk_window_set_opacity or cairo_set_source_rgba etc.)
  2. The platform will then first ask the parent to paint itself and that area which will now include the child areas which will no longer be clipped. This is because the platform now knows the child controls are transparent and can analyze the window hierarchy to determine the proper paint order.
  3. Windows will *still* ask the parent for a brush for the background for the child window and use what is returned. So you need to return the HOLLOW_BRUSH under windows, because the control background is already painted to what you want. With that brush, the platform paints the control.

So let us suppose you do not want to do this for because transparency is such a platform specific thing or you do not want to use platform specific fixes. What else could you do?

  1. wxWidgets creates the child controls and they are default opaque.
  2. The platform paints the parent window and that area, depending on the platform, will by default exclude the child control areas and not paint under them.
  3. The child control needs to be painted. It now has to be owner drawn, because some platforms use a brush to paint the background before painting the control and you do not want that because you may not have just a solid background colour. So the child control determines its parent window and adjusts the clip area and offset and passes the paint call to its parent window. The parent window paints the child control background, after which the child control owner draws and paints over that.

That could be done, but it has drawbacks. There are many more painting calls, as for each control the parent window will likely repaint all of itself but only the little area under the control each time would show through. And all those control would have to be owner drawn which is not ideal and very platform specific. Plus what if the parent is transparent too or there are overlapped child windows and such? You can get around the owner painting by using HOLLOW_BRUSH, but then you are in platform specific land again. It would be nice if you could avoid all of that, and hey, you can, but then you need to use the flags provided for this purpose.

I think the reason true transparency does not work in wxWidgets is because it is very platform dependent, it requires several different steps for it to work, and rarely is one developer able to fix something across all platforms. The best alternative is to extend a paradigm that already exists in the code, and eventually similar changes on other platforms will follow as needed.

wxTRANSPARENT_WINDOW is not new, it is just that some code like static text and the radio buttons do not pass this flag up like all the generic wxWidget window code does. And then if you add the HOLLOW_BRUSH piece, you can get transparency for most controls.

This issue continually comes up, and the patch only takes effect when people use wxTRANSPARENT_WINDOW. Otherwise there is no impact. Is it better to find lots of people creating their own classes (like TransparentStaticText) or have tutorials about writing your own transparent platform specific wxWidget controls or have obvious missing functionality, if a few code changes governed by an existing flag can fix that?

Who will be hurt by this patch? Nobody. But several people will benefit from it.

comment:14 Changed 2 years ago by rakeshthp

Well, I believe, one day or the other, this has to be fixed in-to the code. Provided, the patch is properly tested on each and every platform. Now based on testing, the patch can be modified by platform specific macros. I guess that is one way to go ahead. The transparency issue works, will with windows classic appearance on windows XP.

One has to find out what exactly causes the weird behavior when the appearance is changed to default windows XP style (where the text part becomes black)? In my case where I am using wxRadioButton. I haven't gone through the code of wxRadioButton, but just a guess that the flag WS_EX_TRANSPARENT which is passed to wxRadioButton control, should it be also passed to the text part? I mean if there is wxStatixText control used in wxRadioButton control, then, the same flag must also be passed to wxStaticText control. Of course wxBen will be knowing things much more than me, but still it's just a guess.

I would rather agree with wxBen, but there need a series of tests on various platforms before applying this patch.

Good luck :-)

comment:15 Changed 2 years ago by wxBen

For those wanting to make use of the patch in here, please note that the following change is necessary to fix foreground colour changes no longer going through. The SetTextColor line needs to be added:

WXHBRUSH wxControl::MSWControlColor(WXHDC pDC, WXHWND hWnd)
{
    // See if the extended windows style is truly transparent
    DWORD dwExStyle = ::GetWindowLong(hWnd, GWL_EXSTYLE);
    if (dwExStyle & WS_EX_TRANSPARENT)
       {
       //We need to return the HOLLOW_BRUSH, which has a
       //non zero handle. Without this, the code below
       //will typically return a zero handle and false
       //which means Windows will typically default to
       //a white background.
       ::SetTextColor((HDC)pDC, wxColourToRGB(GetForegroundColour()));
       ::SetBkMode((HDC)pDC, TRANSPARENT);
       return (WXHBRUSH)GetStockObject(HOLLOW_BRUSH);
       }

    if ( HasTransparentBackground() )
    {       
        ::SetBkMode((HDC)pDC, TRANSPARENT);
    }

I would submit a more compete patch that also fixes transparency in the radio buttons, but if it wont be integrated, I am less motivated.

comment:16 Changed 2 years ago by wxBen

Thanks to some recent fixes in the wxWidgets code, we are closer to making this work seamlessly, for those who choose to use this option. The second code chunk shown below works on the latest SVN version of the code base. It also works for the checkbox, which then does not need to be owner drawn anymore!

Vadim, is there any way we can somehow make this part of wxWidgets via a flag or option somehow so that you do not need to write your own classes? What are you willing to accept?

What if I add a method called say MSWUseHollowBrush(bool enable=true) to wxControl which changes the default behavior of MSWControlColor to that shown below?

What if we can just get this little piece (which is all that is needed now) in src\msw\control.cpp:

WXHBRUSH wxControl::MSWControlColor(WXHDC pDC, WXHWND hWnd)
{
    // See if the extended windows style is truly transparent
    DWORD dwExStyle = ::GetWindowLong(hWnd, GWL_EXSTYLE);
    if (dwExStyle & WS_EX_TRANSPARENT)
       {
       wxColour fgColour = GetForegroundColour();
       ::SetTextColor((HDC)pDC, RGB(fgColour.Red(), fgColour.Green(), fgColour.Blue()));
       ::SetBkMode((HDC)pDC, TRANSPARENT);
       return (WXHBRUSH)GetStockObject(HOLLOW_BRUSH);
       }

Here is the code that works on the current latest wxWidgets code base:

wxStaticText* text = new wxStaticTextTransparent(parent, eventId, label, wxDefaultPosition, wxDefaultSize, wxTRANSPARENT_WINDOW);

wxCheckBox* checkBox = new wxCheckBoxTransparent(parent, eventId, label, wxDefaultPosition, wxDefaultSize, wxTRANSPARENT_WINDOW);

wxRadioButton* rButton = new wxRadioButtonTransparent(parent, eventId, label, wxDefaultPosition, wxDefaultSize, wxTRANSPARENT_WINDOW);

------------------------------------------------------------------------------
class wxStaticTextTransparent : public wxStaticText

{
public:

wxStaticTextTransparent(wxWindow *parent,

wxWindowID id,
const wxString& label,
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = 0,
const wxString& name = wxStaticTextNameStr)

:wxStaticText(parent, id, label, pos, size, style, name)
{
}

virtual WXHBRUSH MSWControlColor(WXHDC pDC, WXHWND hWnd)

{
wxColour fgColour = GetForegroundColour();
::SetTextColor((HDC)pDC, RGB(fgColour.Red(), fgColour.Green(), fgColour.Blue()));
::SetBkMode((HDC)pDC, TRANSPARENT);
return (WXHBRUSH)GetStockObject(HOLLOW_BRUSH);
}

};

------------------------------------------------------------------------------
class wxCheckBoxTransparent : public wxCheckBox

{
public:

wxCheckBoxTransparent(wxWindow *parent,

wxWindowID id,
const wxString& label,
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = 0,
const wxValidator& validator = wxDefaultValidator,
const wxString& name = wxCheckBoxNameStr)

:wxCheckBox(parent, id, label, pos, size, style, validator, name)
{
}

virtual WXHBRUSH MSWControlColor(WXHDC pDC, WXHWND hWnd)

{
wxColour fgColour = GetForegroundColour();
::SetTextColor((HDC)pDC, RGB(fgColour.Red(), fgColour.Green(), fgColour.Blue()));
::SetBkMode((HDC)pDC, TRANSPARENT);
return (WXHBRUSH)GetStockObject(HOLLOW_BRUSH);
}

};

------------------------------------------------------------------------------
class wxRadioButtonTransparent : public wxRadioButton

{
public:

wxRadioButtonTransparent(wxWindow *parent,

wxWindowID id,
const wxString& label,
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = 0,
const wxValidator& validator = wxDefaultValidator,
const wxString& name = wxRadioButtonNameStr)

:wxRadioButton(parent, id, label, pos, size, style, validator, name)
{
}

virtual WXHBRUSH MSWControlColor(WXHDC pDC, WXHWND hWnd)

{
wxColour fgColour = GetForegroundColour();
::SetTextColor((HDC)pDC, RGB(fgColour.Red(), fgColour.Green(), fgColour.Blue()));
::SetBkMode((HDC)pDC, TRANSPARENT);
return (WXHBRUSH)GetStockObject(HOLLOW_BRUSH);
}

};

}}}

comment:17 Changed 2 years ago by vadz

Hmm, would it make sense to use the alpha component of background colour instead of the style? We could return HOLLOW_BRUSH from wxControl::DoMSWControlColor() if colBg.Alpha() == wxALPHA_TRANSPARENT, I think.

comment:18 Changed 2 years ago by wxBen

Vadim,

I tried "colBg.Alpha() == wxALPHA_TRANSPARENT" in DoMSWControlColor which I assume also requires a line like this when creating the control:

text->SetOwnBackgroundColour(wxTransparentColour);

However, setting the background colour in this manner has other side effects somewhere and it either is no longer transparent or turns completely black. I did not have any luck determining where this is causing a problem.

I also experimented with using the style wxBG_STYLE_TRANSPARENT, but that also seems to have other consequences as everything just goes black then. Also, this option is listed in the code as "Mac-only style", so presumable we should not try and use it for something else.

The only option that seems to work nicely and not have any unexpected side effects is in the latest patch attached. Please consider it and advise.

Changed 2 years ago by wxBen

TransCheckBoxStaticText.diff

comment:19 Changed 2 years ago by vadz

  • Milestone set to 3.0

comment:20 Changed 23 months ago by vadz

Rereading all this once again, I wonder if there are any drawbacks in always using WS_EX_TRANSPARENT and HOLLOW_BRUSH with wxStaticText and similar controls? I.e. instead of testing for wxTRANSPARENT flag just test for HasTransparentBackground()?

comment:21 Changed 17 months ago by vadz

To return to this, I'd like to ask everybody interested in making this work to please test whether turning on WS_EX_TRANSPARENT in wxWindow::MSWCreate() if HasTransparentBackground() returns true results in any unwanted effects.

TIA!

comment:22 Changed 14 months ago by vadz

  • Resolution set to wontfix
  • Status changed from new to closed

I've done some testing myself and the trouble is that this doesn't even work in the cases where it's supposed to :-( E.g. non-owner-drawn checkboxes don't have the transparent background in the ribbon and disappear completely in the toolbar (see #12307 which I hoped this would help with).

So this really doesn't help at all. We probably should return HOLLOW_BRUSH for the controls with transparent background but I have trouble seeing how can WS_EX_TRANSPARENT help with everything. According to its documentation (and supported by Raymond Chen), it shouldn't change anything at all as it is only supposed to change the order of painting of the sibling windows and we don't have any overlapping siblings whatsoever. Clearly, it does change something though, maybe there is also some interaction with DeferWindowPos()? But it doesn't seem to help for all that.

comment:23 Changed 14 months ago by vadz

Even returning HOLLOW_BRUSH, i.e. applying this patch:

  • src/msw/control.cpp

    diff --git a/src/msw/control.cpp b/src/msw/control.cpp
    index 1fe94ba..e06d230 100644
    a b WXHBRUSH wxControl::DoMSWControlColor(WXHDC pDC, wxColour colBg, WXHWND hWnd) 
    427427WXHBRUSH wxControl::MSWControlColor(WXHDC pDC, WXHWND hWnd) 
    428428{ 
    429429    if ( HasTransparentBackground() ) 
     430    { 
     431        // For transparent controls we must tell the system to just not erase 
     432        // the background at all, otherwise they wouldn't be transparent. 
     433        ::SetTextColor((HDC)pDC, wxColourToRGB(GetForegroundColour())); 
    430434        ::SetBkMode((HDC)pDC, TRANSPARENT); 
     435        return (WXHBRUSH)::GetStockObject(HOLLOW_BRUSH); 
     436    } 
    431437 
    432438    // don't pass any background colour to DoMSWControlColor(), our own 
    433439    // background colour will be used by it only if it is set, otherwise the 

can't be done because it breaks the currently working transparency of wxStaticText and wxCheckBox as the parent background doesn't show through them any more.

So this is really not the correct approach, we must paint the background ourselves with MSW.

Note: See TracTickets for help on using tickets.