kirsan31/winforms-datavisualization

PerMonitorV2 DPI mode support

Opened this issue Β· 14 comments

First of all: thank you for this porting.

I have created a very simple form with C# .NET 7:

image

WinForms.DataVisualization is at version 1.7.0 (Latest stable by the NuGet manager).

When I run this application on standard DPI (96, 100% scale) screens it works as intended:

image

However after I move the window to a different DPI (240, 250% scale), all labels stays small:

image

To illustrate the problem better, I scaled down this last image back to 100% (please don't consider the borders, lines, etc, focus only to labels on legend, and around the chart):

image

Can we somehow keep the consistency between DPI changes?

First of all: thank you for this porting.

πŸ™

Labels are not rescaled in higher DPI displays

I can't reproduce this behavior. Fresh app with PerMonitorV2 enabled, on the fly DPI changing results:
image

Thx for the quick heads up! I can confirm, works as intended if I change the monitor from x to y scale while application is running.

But that's not too real life scenario, i.e: I only change scale once per 10 years :)

However, if you have multiple monitors with different scale (100, 250), and you move the app between them, that's I would call a real life scenario.

And that scenario is not working. Lets move the app from 100% display to any other with different scaling.

WoW 😯It's very surprising...

What we have on this topic:

  1. This is not chart problem, this is WinForms problem.
  2. In theory changing settings on the fly must be equal to drugging app on other screen.
  3. In practice, I heard that there were cases where dragging worked, but changing settings did not.
  4. And now we have opposite behavior πŸ™„

Currently we have an interesting discussion on this topic here (read down from the linked post). You can join, or post an example application with this behavior here and I'll mention it there...

P.s. I have only one monitor therefore can test DPI related things only with settings change.

I kindof "fixed" this with using "DpiChangedBeforeParent" and "DpiChangedAfterParent" events.

I kindof "fixed" this with using "DpiChangedBeforeParent" and "DpiChangedAfterParent" events.

@danergo, Those events available for PermonV2 mode applications. And DataVisualization chart is ported only to support existing applications and we are not investing further to support higher DPI modes for this control. If labels here coming from that, explicit handling of those events is the right way to go.

@dreddy-work
The main question here why it's working with on the fly dpi change then?

I found another very strange behavior:

2 displays:
-250% (primary)
-100%

All fonts and lines (borders) MUST be manually rescaled to 100/250. I.e. font: 8.25F/2.5 to look correctly on the not-primary screen.

It seems as if the charting mechanism stores the primary Dpi, and it renders everything with that Dpi, and WinForms doesn't handle the Dpi change; SO in user code we have to deal with it (but in C# WinForms the Primary screen's Dpi value is not accessible).

Is this behavior the same with on the fly dpi change?
If no (all working good), then we still need an answer from @dreddy-work...
And if yes, then I didn't get second part of your question πŸ™ƒ

Similar!

On the fly dpi change I could solved by manually setting all fonts and borders to their inital size * DeviceDpi/96.

It worked perfectly, until a point my primary display is 100%.
When my primary display is not 100%, setting a font to 8.25 * DeviceDpi/96 would make it extremely big on all screens.
If I don't messing with the manual sizing, and leave everything as-is, chart labels are fine on the primary screen only, all other screen they look either way too big or way too small.

Sorry man - still didn't get it :(

I can confirm, works as intended if I change the monitor from x to y scale while application is running.

vs

On the fly dpi change I could solved by manually setting all fonts and borders to their inital size * DeviceDpi/96.

πŸ€·β€β™‚οΈ

let's try step by step what is not working - may me some video, or repro app (only on the fly change problems - I have no 2 monitor)?

No worries, I'm going to summarize here all my findings.

Prerequisites:

  • 2 displays with different scale (i.e their Dpi is different)
  • Compile the sample with DotNet SDK7
  • Define PerMonitorV2 in csproj
  • WinForms.DataVisualization version 1.7.0

ChartTest.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net7.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
    <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="WinForms.DataVisualization" Version="1.7.0" />
  </ItemGroup>

</Project>

Form1.cs:

namespace ChartTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    }
}

Form1.Designer.cs:

namespace ChartTest
{
    partial class Form1
    {
        /// <summary>
        ///  Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        ///  Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        ///  Required method for Designer support - do not modify
        ///  the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea1 = new System.Windows.Forms.DataVisualization.Charting.ChartArea();
            System.Windows.Forms.DataVisualization.Charting.Legend legend1 = new System.Windows.Forms.DataVisualization.Charting.Legend();
            System.Windows.Forms.DataVisualization.Charting.Series series1 = new System.Windows.Forms.DataVisualization.Charting.Series();
            System.Windows.Forms.DataVisualization.Charting.DataPoint dataPoint1 = new System.Windows.Forms.DataVisualization.Charting.DataPoint(0D, 1D);
            System.Windows.Forms.DataVisualization.Charting.DataPoint dataPoint2 = new System.Windows.Forms.DataVisualization.Charting.DataPoint(0D, 4D);
            System.Windows.Forms.DataVisualization.Charting.DataPoint dataPoint3 = new System.Windows.Forms.DataVisualization.Charting.DataPoint(0D, 2D);
            this.chart1 = new System.Windows.Forms.DataVisualization.Charting.Chart();
            this.label1 = new System.Windows.Forms.Label();
            ((System.ComponentModel.ISupportInitialize)(this.chart1)).BeginInit();
            this.SuspendLayout();
            // 
            // chart1
            // 
            chartArea1.Name = "ChartArea1";
            this.chart1.ChartAreas.Add(chartArea1);
            legend1.Name = "Legend1";
            this.chart1.Legends.Add(legend1);
            this.chart1.Location = new System.Drawing.Point(12, 12);
            this.chart1.Name = "chart1";
            series1.ChartArea = "ChartArea1";
            series1.Legend = "Legend1";
            series1.Name = "Series1";
            series1.Points.Add(dataPoint1);
            series1.Points.Add(dataPoint2);
            series1.Points.Add(dataPoint3);
            this.chart1.Series.Add(series1);
            this.chart1.Size = new System.Drawing.Size(300, 300);
            this.chart1.TabIndex = 0;
            this.chart1.Text = "chart1";
            // 
            // label1
            // 
            this.label1.AutoSize = true;
            this.label1.Location = new System.Drawing.Point(318, 25);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(166, 15);
            this.label1.TabIndex = 1;
            this.label1.Text = "Series1 (Label for comparison)";
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
            this.ClientSize = new System.Drawing.Size(527, 332);
            this.Controls.Add(this.label1);
            this.Controls.Add(this.chart1);
            this.Name = "Form1";
            this.Text = "Form1";
            ((System.ComponentModel.ISupportInitialize)(this.chart1)).EndInit();
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.DataVisualization.Charting.Chart chart1;
        private Label label1;
    }
}

This is how my designer looks like:

image

Testcase 1:

Displays:
image

I started 3 instances, and put one onto each display:

image

As you can see, "Series1" label on the chart, on the 250% display is way smaller than the label for comparison, which initially has the same size (and on the primary screen it looks right).

This testcase proves that the Chart control is not able to rescale itself in case of Dpi changes due to moving it to a different display (with different Dpi).

Therefore, I have made a fix into Form's DpiChanged event:

private void Form1_DpiChanged(object sender, DpiChangedEventArgs e)
{
    chart1.Legends[0].Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F * e.DeviceDpiNew / 96, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
}

Testcase 2:
With the fix above, I have made a same screenshot with 3 instances:

image

You can see, now the Legend's text's size is matching with the test label's size. Please note also, that other texts on chart are still small.

Now, let's do some further testing.

Testcase 3:
Let's start the instances on the 250% display (Number1 - it's still not the primary!):

image

Please note: instances from left to right:

  • instance1: looks OK (as Form's DpiChange event fixes the font)
  • instance2: looks OK (as Form's DpiChange event fixes the font)
  • instance3: looks BAD (as we start the instances on this screen, there is no DpiChange event happened on this instance)

Okay, so let's call my "font fixer" in the Form's OnLoad too:

private void Form1_Load(object sender, EventArgs e)
{
    chart1.Legends[0].Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F * DeviceDpi / 96, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
}

Testcase 4:
Retesting with the above fix:

image

All looks great now.

Let's do some further testing with this.

Testcase 5:

Displays:

image

Let's start the instances on the primary screen, and move place them the same as before:

image

You can see, that legend's font is now oversized, BUT other texts on the chart looks good (ONLY on primary screen!)

Result: my font fixer doesn't work if the primary screen is not 100%. It seems the font size change to DeviceDpi / 96 is not sufficient, because font size need to be set by taking into account also the primary screen's Dpi.

So after a crazy amount of digging for the information for the primary screen's Dpi, I changed font fixer to:

chart1.Legends[0].Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F * DeviceDpi / PrimaryScreenDpi, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);

And retested testcase4, and testcase5:

Testcase 4 retest:
image

Testcase 5 retest:
image

These are my findings, and unfortunately this also affects border sizes on both the chart axes and on linechart lines. Technically all line widths has to be corrected the same way as the fontsize I've shown above without the primary dpi:

So, the correct scaling for fonts is Current_Dpi_which_displays_the_chart / Primary_screens_dpi, and for line widths: Current_Dpi_which_displays_the_chart / 96.

Please note: none of these testing involved Dpi change or Primary display change during runtime. These changes were only done when the application was not running.

I hope now it's a bit clearer, let me know if there is anything unclear parts still. :)
Thank you!

@danergo
Came to say thank you for the detailed report. As soon as I have time, I will definitely try to figure it out...

@danergo

Ok, I've spend some time to dig into this...
And found that chart control simple not support PerMonitorV2 at all 😭
In this test I set up PerMonitorV2 in project file, but forgot to call ApplicationConfiguration.Initialize(); πŸ€¦β€β™‚οΈSo app was running in SystemAware mode. And this increased font is simple zooming...

That is. Only SystemAware out of the box, or manually support PerMonitorV2, like you tried, until PerMonitorV2 will be implements internally (I hope it will).

@danergo

I kindof "fixed" this with using "DpiChangedBeforeParent" and "DpiChangedAfterParent" events.

Hi, I wander - how you was able to fix this?
As I can see, that all scaling done around Graphics.DpiX/DpiY and Graphics doesn't support PerMonitorV2 - DpiX always return app start dpi.

----UPD----

Oh, I see - your mostly talked about fonts. Fonts are one side of the problem, we also need to scale graphics...