-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Add support for High DPI displays to wxAgg backend #26710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
pilimg = PIL.Image.open(cbook._get_data_path("images", name)) | ||
# ensure RGBA as wx BitMap expects RGBA format | ||
image = np.array(pilimg.convert("RGBA")) | ||
try: | ||
dark = wx.SystemSettings.GetAppearance().IsDark() | ||
except AttributeError: # wxpython < 4.1 | ||
# copied from wx's IsUsingDarkBackground / GetLuminance. | ||
bg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) | ||
fg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) | ||
# See wx.Colour.GetLuminance. | ||
bg_lum = (.299 * bg.red + .587 * bg.green + .114 * bg.blue) / 255 | ||
fg_lum = (.299 * fg.red + .587 * fg.green + .114 * fg.blue) / 255 | ||
dark = fg_lum - bg_lum > .2 | ||
if dark: | ||
fg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) | ||
black_mask = (image[..., :3] == 0).all(axis=-1) | ||
image[black_mask, :3] = (fg.Red(), fg.Green(), fg.Blue()) | ||
return wx.Bitmap.FromBufferRGBA( | ||
image.shape[1], image.shape[0], image.tobytes()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this lose the compatibility with dark mode? I never could figure out how to test it, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@QuLogic Thank you for your review. You are correct.
I switched from reading the icons from png
format to svg
format so they will appear sharp in all DPIs.
As you point out and I mentioned in the additional details, this format switch has the caveat that this "black theme" code doesn't work as it is because PIL has no support for svg. Since I have never seen this code execute in practice and I also had not way to test it, I decided the tradeoff of adding sharp icons and removing this code was acceptable. But others may have other opinions.
If someone who uses and can test the "black theme" functionality in hidpi could suggest how to best add black theme support back while reading the icons in SVG format we could add it as part of this PR. Alternatively, it could also be added later as a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a very quick look, given that we generate the SVG icons ourselves, I think we can guarantee that their color can be switched with a simple string replacement, and thus it should be doable to generate the proper-colored SVG icons on the fly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @anntzer I just looked into this!
Currently our monochromatic svg icons --which are the ones that need changing for black themes-- do not explicitly specify foreground color, so the foreground always appears black since that is the svg default. The background is explicitly specified as transparent which does not need changing for black themes.
I propose that we submit a separate PR making the foreground explicitly black in all monochromatic icons. I just tested this and it is straightforward: It simply requires adding style="fill:black;"
in one spot of each svg file.
If the foreground color is explicitly listed in the svg, then adding support for black theme in backends that use svg icons simply involves doing a string replacement fill:black
-> fill:white
as you suggest.
If you think that this is a reasonable path, I will submit a PR with the icon changes right away, and restore support for black themes in this PR assuming the foreground is explicitly listed as black in monochromatic svg icons.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks reasonable to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anntzer Thank you for your quick reply!
Finished with the changes.
- I just restored black theme support in the
wxAgg
backend in this PR. - I explicitly set the foreground color of svg icons to black in a separate PR: Explicitly set foreground color to black in svg icons #26731
I tested both changes together and they work as expected.
I squashed the commits to not pollute history |
Is there anything I can do to help merge this? |
I gave this a try on Windows, but it didn't seem to work. According to your links, the application needs to set HiDPI awareness in its manifest. We can't really do that however given that we aren't an application, so I think we might need to abstract out the runtime-setting of the awareness from I unfortunately do not have a Linux HiDPI setup at the moment to test. |
Hello @QuLogic Thank you for spending time on this:
From the MSW documentation a Windows script/application that wants to handle its own scaling --instead of letting the OS do the scaling in a generic way with blurry results-- must let the OS know. This is true regardless of the language the application is written in or any frameworks used. In python this is achieved with the following code: import ctypes
ctypes.windll.shcore.SetProcessDpiAwareness(1) My understanding is that this code should be part of the end user's application or script, not set by libraries (e.g. matplotlib) or frameworks (e.g. wx) because it is the user who knows if all the libraries and components they use are capable of handling their own scaling. If the end user set's it, wx, matplotlib and other graphical components should read the actual DPI value and handle scaling in a library specific manner. wx already behaves this way. I believe this PR makes matplotlib behave this way when used in conjunction with wx. If you have a chance, please try adding these lines to your test script and let me know the results. If you encounter any issues I can work with you on them. Thank you. Additional notes for the record
|
When Matplotlib is the one managing windows (i.e., through |
This makes sense to me
In
This code calls the function defined at To achieve the same functionality in the wx backend It would be trivial to add the following lines to the wx backend:
But I do not think in the wx backend case it makes sense to call these lines unconditionally because the typical use of matplotlib with the wx backend is to have figures embedded in larger wx applications rather than in stand alone windows. So I think we need to let the end user retain control of whether dpi awareness should be handled by their application or the OS. What do you think? If you agree we could add these lines as a function to the wx backend that the user can invoke if they so choose. Or perhaps better, to avoid redundancy, we could document the functionality and have the user call the original MSW |
Is there anything I can do to help merge this? |
The change is based on matplotlib/matplotlib#26710
Thank you @tianzhuqiao for testing this, and for your feedback. I don't have access to a Mac, but I am going to look into it and try to fix it and, if you don't mind, I will ask you to please test on a Mac. Thank you in advance. I am currently traveling, so it may take me a few weeks to get back to you. |
@jmoraleda, sure, no problem. And matplotlib/lib/matplotlib/backends/backend_wx.py Line 1124 in e470c70
|
@tianzhuqiao I believe my last commit fixes the |
The tests failed because of a problem with a new version of pytest which has now been yanked. I’m going to close and re-open to restart the CI. |
Thanks @jmoraleda , |
I am a bit curious what this assertion is based on. My expectation is the opposite in that independent of backend we are going to have many more users of
If we go through matplotlib/lib/matplotlib/backends/backend_wx.py Lines 43 to 48 in 9618fc6
A similar bit of logic should probably be pushed to IPython in the case that they make the App. [edited to fix markup] |
Brief answerI think my last commit does the right thing in all cases that we have discussed :-). Please test if you have a chance! Longer answer@tacaswell Thank you for reviewing this and especially for bringing I had not paid attention to the fact that this function is only invoked if a
I just pushed a commit that sets per-monitor dpi awareness inside Now typical matplotlib code such as: import matplotlib
matplotlib.use('wxAgg')
import matplotlib.pyplot as plt
plt.plot([1,2,3,4,5])
plt.show() just works out of the box taking advantage of the full monitor resolution as expected. While wx users that embed matplotlib figures in their applications can still set dpi as they wish. RemarkPer MSW documentation, once a process makes an API call to choose how to handle dpi awareness, future calls will be ignored and the initial setting will not be changed:
This is why we shouldn't always set per-process dpi awareness when using the wx backend, since there are applications that do not implement support to handle this in their non-matplotlib windows and would thus break if the O/S expected them to. |
The remaining linter warning is about a line that is too long. That line is a comment containing a url to MS documentation on the topic of setting dpi awareness. I think it is useful to retain it for future reference, but let me know what you think. Thank you. |
Are there any outstanding issues or questions? Is there anything I can do to help merge this? |
@@ -45,6 +43,10 @@ def _create_wxapp(): | |||
wxapp = wx.App(False) | |||
wxapp.SetExitOnFrameDelete(True) | |||
cbook._setup_new_guiapp() | |||
if wx.Platform == '__WXMSW__': | |||
# Set per-process DPI awareness. See https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
flake8 will be happy if the long URL is on a line by itself:
# Set per-process DPI awareness. See https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness | |
# Set per-process DPI awareness. For more details, see | |
# https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you. Good to know. I ended up removing the link because using the internal function that you suggest in your next comment makes it unnecessary.
import ctypes | ||
ctypes.windll.shcore.SetProcessDpiAwareness(2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use the wrapper from _c_internal_utils
as I stated before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@QuLogic Thank you for catching that. Fixed.
It's a bit unfortunate that AppVeyor was already in a broken state, because that hid that this was failing:
|
Fixes AppVeyor failure noted in matplotlib#26710 (comment)
@QuLogic What is the best way of running locally the exact appveyor command that fails? When I run
|
You should be able to run any script and set |
Oh also, if you're getting those errors when running pytest, it appears you've not installed Matplotlib correctly, and probably have some old copy in your environment. |
I was able to reproduce the problem of the missing toolmanager icons running
but that is fixed now with #28007, which I think we should merge. But #28007 does not fix the appveyor problem and I don't know how to run |
Very possible. My default installation of matplotlib in debian is via apt and the package manager, and my earlier run of the tests was on a patched version of that that includes the latest wx backend code, instead of in a clean installation of all the latest code in a |
No tests ran at all, as it failed to import. |
Summary
This PR adds support to the
wxAgg
backend to corrertly render plots in high DPI displays in all three platforms that the wx toolkit supports: Linux, MACOS and Windows.This PR closes issue #7720
Background
This PR adds support for high DPI displays using the underlying toolkit support for high DPI displays: For information of wxPython support for High DPI displays, see https://docs.wxpython.org/high_dpi_overview.html
The wxPython toolkit is built on top of of the C++ wxWidgets toolkit. For a more detailed explanation of support for high DPI displays in the wx ecosystem see the WxWidgets explanation: https://docs.wxwidgets.org/3.2/overview_high_dpi.html
Additional details
This PR correctly scales plots in high DPI displays, including figure size, font size and marker and line width. This PR also includes reading toolbar icons from svg instead of png to make them sharp at all dpi's.
I remark this last feature comes with the caveat that automatically changing icon colors for dark themes is not done.[The limitation mentioned in the previous sentence has been removed in the current version of this PR. See discussion below for details. I am crossing out the sentence and adding this note, instead of removing the sentece so the discussion still makes sense]Additional testing
The variety of use cases and features of matplotlib is very large. I welcome additional testing in all platforms as it is conceivable that there may be features that I have not tested and that I have not added support for. Thank you.