How to scale on high-DPI the image of a Windows Form Button?
Asked Answered
B

2

9

I cannot find a way to make scale the image in a Windows Form Button. See below what it looks like with the Windows Form designer shown on DPI 200% (I am aware that the Windows Form designer should be only used on DPI 100% / 96, this screenshot just illustrates properly my point).

While the button size get scaled properly (34x33), the image in the button size doesn't get scaled/stretched/zoomed (it remains 16x16). I did many attempt to solve this:

  • The parent control AutoScaleMode is set to Font, setting it to Dpi doesn't make this work.
  • Setting button AutoSize to true or false doesn't make it work.
  • Setting button or parent controlAutoSizeMode to any value doesn't make it work.
  • there is no Button.ImageLayout that could be set to Stretch or Zoom.
  • Using the new App.Config setting <add key="EnableWindowsFormsHighDpiAutoResizing" value="true" /> doesn't make it work.
  • Changing button FlatStyle or ImageAlign doesn't make it work.

How did you get solved this in your app?

Windows Form Button Image doesn't scale

Bergren answered 12/11, 2014 at 10:17 Comment(6)
You are asking for a pony. You can easily add the code to scale the bitmaps, the only guarantee you'll get is that you'll absolutely hate the way it looks.Crowned
Creating a new bitmap and scaling it myself is the hacky/plan B solution of course. This is the control responsibility to stretch its image and here, I really want to make sure I didn't miss a clean solution.Bergren
That's the mysterious part of this question. If you know that bitmap scaling is an ugly hack then why on Earth would you expect a standard .NET class to use it? That "responsibility" you expect is simply not there. Managing assets for a dpiAware program is a programmer's job. Implementing it is pretty trivial but you'll never get any help as long as you ask the question this way.Crowned
Here we are turning the question into philosophy. Several others Windows Form controls have facilities to stretch their images (PictureBox, DataGridViewImageColumn...). My question is: does Button has such facility I might have missed? If I have to implement myself then I consider it is plan B, or you say, ugly. Of course bitmap stretch doesn't render super nice, but it doesn't look as buggy as when the image is four time smaller than what it should be...Bergren
... the plan AAA would be of course to have various size for each image, chosen according the current DPI and then writing extra code would make sense. Nowadays 4K monitors becomes affordable, and users expect to have software that scales well on hi-res. This is becoming urgent and first step is implementing plan A, un-buggy UI with stretched image. The second step will be plan AAA. Btw VS2013 and below implemented plan A and VS2015 will implement plan AAA. So to rephrase my question: is there an out-of-the-box way to implement plan A, until later we get time to implement plan AAA.Bergren
More philosophy. MS is going toward out-of-the-box stretched images for Windows Form UI when hi-Res. .NET Fx v4.5.2 offer the new EnableWindowsFormsHighDpiAutoResizing setting that modifies some Windows Form controls behavior to stretch images when hi-Res. infoq.com/news/2014/05/DotNet-4-5-2 This is an acceptable plan A, not as good as plan AAA that requires extra work. This MS direction makes clear this question is relevant and whatever the definitive answer, it will help developers that will wonder if Buttons with an image needs extra code or not to look ok on hi-Res.Bergren
B
9

So despite the MS philosophy is to go toward out-of-the-box stretched images for Windows Form Controls when high DPI, it seems images on Button need to be stretched manually. Of course an even better solution would be that, for each bitmap shown to user (on button and everywhere else) to define several bitmaps adapted to 250% 200% 150% and 125% DPI.

Here is the code:

  public static IEnumerable<IDisposable> AdjustControlsThroughDPI(this Control.ControlCollection controls) {
     Debug.Assert(controls != null);
     if (DPIRatioIsOne) {
        return new IDisposable[0]; // No need to adjust on DPI One
     }

     var list = new List<IDisposable>();
     foreach (Control control in controls) {
        if (control == null) { continue; }

        var button = control as ButtonBase;
        if (button != null) {
           button.AdjustControlsThroughDPI(list);
           continue;
        }

        // Here more controls tahn button can be adjusted if needed...

        // Recursive
        var nestedControls = control.Controls;
        Debug.Assert(nestedControls != null);
        if (nestedControls.Count == 0) { continue; }
        var disposables = nestedControls.AdjustControlsThroughDPI();
        list.AddRange(disposables);
     }
     return list;
  }

  private static void AdjustControlsThroughDPI(this ButtonBase button, IList<IDisposable> list) {
     Debug.Assert(button != null);
     Debug.Assert(list != null);
     var image = button.Image;
     if (image == null) { return; }

     var imageStretched = image.GetImageStretchedDPI();
     button.Image = imageStretched;
     list.Add(imageStretched);
  }


  private static Image GetImageStretchedDPI(this Image imageIn) {
     Debug.Assert(imageIn != null);

     var newWidth = imageIn.Width.MultipliedByDPIRatio();
     var newHeight = imageIn.Height.MultipliedByDPIRatio();
     var newBitmap = new Bitmap(newWidth, newHeight);

     using (var g = Graphics.FromImage(newBitmap)) {
        // According to this blog post http://blogs.msdn.com/b/visualstudio/archive/2014/03/19/improving-high-dpi-support-for-visual-studio-2013.aspx
        // NearestNeighbor is more adapted for 200% and 200%+ DPI
        var interpolationMode = InterpolationMode.HighQualityBicubic;
        if (s_DPIRatio >= 2.0f) {
           interpolationMode = InterpolationMode.NearestNeighbor;
        }
        g.InterpolationMode = interpolationMode;
        g.DrawImage(imageIn, new Rectangle(0, 0, newWidth, newHeight));
     }

     imageIn.Dispose();
     return newBitmap;
  }

Notice that an enumerable of disposable bitmaps created is returned. If you don't care disposing bitmap on buttons, you won't have to care for disposing stretched bitmap.

Notice we dispose original buttons bitmaps.

Notice our own members to deal with DPI: MultipliedByDPIRatio(this int) , DPIRatioIsOne:bool , s_DPIRatio. You can write your own, the tricky point is to obtain the actual DPI ratio. To gather DPI ratio the best way I found is this one.

Notice the reference to the blog post Improving High-DPI support for Visual Studio 2013 where the VS team explains that for their icon style, they determine that image stretched between ] 200%, 100% [ is best achieved with Bicubic algorithm, and above or equal to 200%, is best achieved with naive nearest neighbor algorithm. The code presented reflects these choices.


Edit: below screenshot of various interpolation mode at 200% DPI, IMHO InterpolationMode.HighQualityBicubic is better than InterpolationMode.NearestNeighbor.

Interpolation mode

Bergren answered 13/11, 2014 at 10:58 Comment(0)
D
7

Here is a ready to use helper class based on the accepted answer that includes retrieval of the DPI scale, and adds support of PictureBox image scaling:

public static class HighDpiHelper
{
    public static void AdjustControlImagesDpiScale(Control container)
    {
        var dpiScale = GetDpiScale(container).Value;
        if (CloseToOne(dpiScale))
            return;

        AdjustControlImagesDpiScale(container.Controls, dpiScale);
    }

    private static void AdjustButtonImageDpiScale(ButtonBase button, float dpiScale)
    {
        var image = button.Image;
        if (image == null)
            return;

        button.Image = ScaleImage(image, dpiScale);
    }

    private static void AdjustControlImagesDpiScale(Control.ControlCollection controls, float dpiScale)
    {
        foreach (Control control in controls)
        {
            var button = control as ButtonBase;
            if (button != null)
                AdjustButtonImageDpiScale(button, dpiScale);
            else
            {
                var pictureBox = control as PictureBox;
                if (pictureBox != null)
                    AdjustPictureBoxDpiScale(pictureBox, dpiScale);
            }

            AdjustControlImagesDpiScale(control.Controls, dpiScale);
        }
    }

    private static void AdjustPictureBoxDpiScale(PictureBox pictureBox, float dpiScale)
    {
        var image = pictureBox.Image;
        if (image == null)
            return;

        if (pictureBox.SizeMode == PictureBoxSizeMode.CenterImage)
            pictureBox.Image = ScaleImage(pictureBox.Image, dpiScale);
    }

    private static bool CloseToOne(float dpiScale)
    {
        return Math.Abs(dpiScale - 1) < 0.001;
    }

    private static Lazy<float> GetDpiScale(Control control)
    {
        return new Lazy<float>(() =>
        {
            using (var graphics = control.CreateGraphics())
                return graphics.DpiX / 96.0f;
        });
    }

    private static Image ScaleImage(Image image, float dpiScale)
    {
        var newSize = ScaleSize(image.Size, dpiScale);
        var newBitmap = new Bitmap(newSize.Width, newSize.Height);

        using (var g = Graphics.FromImage(newBitmap))
        {
            // According to this blog post http://blogs.msdn.com/b/visualstudio/archive/2014/03/19/improving-high-dpi-support-for-visual-studio-2013.aspx
            // NearestNeighbor is more adapted for 200% and 200%+ DPI

            var interpolationMode = InterpolationMode.HighQualityBicubic;
            if (dpiScale >= 2.0f)
                interpolationMode = InterpolationMode.NearestNeighbor;

            g.InterpolationMode = interpolationMode;
            g.DrawImage(image, new Rectangle(new Point(), newSize));
        }

        return newBitmap;
    }

    private static Size ScaleSize(Size size, float scale)
    {
        return new Size((int)(size.Width * scale), (int)(size.Height * scale));
    }
}
Depict answered 30/3, 2017 at 7:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.