leocb / MaterialSkin

Theming .NET WinForms, C# or VB.Net, to Google's Material Design Principles.
MIT License
446 stars 132 forks source link

Strange bugs with position of TabControl menu #229

Open HavenDV opened 3 years ago

HavenDV commented 3 years ago

https://user-images.githubusercontent.com/3002068/135720568-3f4610aa-6104-4e3f-99a2-9356c83db111.mp4

https://user-images.githubusercontent.com/3002068/135720566-bed3f242-664f-4607-9059-239ecd341bc7.mp4

HavenDV commented 3 years ago

The problem manifests itself on computers with multiple monitors and different resolutions/scales.

orapps44 commented 3 years ago

Hi,

I've tried with multiple minitors with different reloutions on windows 10 but I've not been able to reproduce it.

Are you using last release or current master ? Do you have similar behaviour with MaterialSkin Demo ?

HavenDV commented 3 years ago

image I have three monitors - 1920 + 3940 + 1920. The second and third are set to 150% for this value.

HavenDV commented 3 years ago

Video using an example from the latest master code: The second screen(3940 - 150%):

https://user-images.githubusercontent.com/3002068/135733355-02353965-02a9-4b76-a37c-27de3ebdcc15.mp4

The third screen(1920 - 150%)

https://user-images.githubusercontent.com/3002068/135733357-323de3d9-f9e9-4ccc-9c46-f37aee7adad3.mp4

orapps44 commented 3 years ago

Ok I've been able to reproduce it. It seems to be related to monitor scale when greater than 100%. When moving main form to another monitor with different scale, then drawer form scale isn't modified at the same time than main form.

leocb commented 3 years ago

This is an issue with the dpi awareness of the lib, the form follows the dpi of whatever monitor most of it is over at, since the menu is another form, this happens, honestly it's going to be a nightmare to solve, if possible at all

orapps44 commented 3 years ago

Hi @leocb , I was thinking about using a panel docked to left side and add drawer form to it. This way drawer form is allways attached to main form. And that may also makes drawer scrolling easier.

leocb commented 3 years ago

AFAIK you can't add a form inside another, I use a form because windows can draw them efficiently when opening or closing, you could solve this by transforming it into a control, but performance would drop by a lot.

VolatilePulse commented 3 years ago

Since I closed my branch and haven't been able to come up with a suitable solution for this yet, I'll share what I've tried so far and what the issue is that's causing this behavior.

I tried replacing all of the drawer forms with actual controls embedded on the material form, but due to limitations of how opacity and transparency work, the only viable option I found was creating a bitmap and effectively applying a fade on top of it. The issue with bitmaps is that they're horribly slow.

All controls added to a material form are also susceptible to the theme manage altering the background color, which I implemented a workaround for, but due to how the controls are drawn, every control paints itself after the drawer scrim. This provides a visual issue where the controls under the scrim are drawn on top of it, but are effectively under it.

unknown.png

Forms are just controls as well and can also be added to a form. It's a technique that we use at work to provide a "screen" and embed other forms in a parent form. I'm not a huge fan of this approach as you lose a lot of the functionality that are granted to forms, like opacity. To do this, you just add the form as a control. The form itself must have TopLevel = false though.

The specific issue represented here is due to the DPI scaling for the main form since it's on a different monitor than the drawer form. The issue resolves itself when the drawer is located on the same monitor, specifically when at least 50% of the drawer form is on that monitor. A solution to this could be to make the drawer form be the same width as the scrim, or overlay, but be 100% transparent or 0 opacity.

I've been attempting different options here because I'd like to return native resize controls to the Material Form, but due to the way the additional drawer forms are used, 100% native resize isn't possible.

HavenDV commented 3 years ago

Have you tried this approach?

    public class TransparentTableLayoutPanel : TableLayoutPanel
    {
        protected override CreateParams CreateParams
        {
            get
            {
                var @params = base.CreateParams;

                @params.ExStyle |= 0x00000020; // WS_EX_TRANSPARENT

                return @params;
            }
        }
    }

or

internal class NativeMethods
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct Point
        {
            public int x;
            public int y;

            public Point(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct Size
        {
            public int x;
            public int y;

            public Size(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        public struct BlendFunction
        {
            public byte BlendOp;
            public byte BlendFlags;
            public byte SourceConstantAlpha;
            public byte AlphaFormat;
        }

        [DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
        public static extern int UpdateLayeredWindow(IntPtr hwnd, IntPtr hdcDst, ref Point pptDst, ref Size psize, IntPtr hdcSrc, ref Point pprSrc, int crKey, ref BlendFunction pblend, int dwFlags);

        [DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
        public static extern IntPtr GetDC(IntPtr hWnd);

        [DllImport("user32.dll", ExactSpelling = true)]
        public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDc);

        [DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
        public static extern IntPtr CreateCompatibleDC(IntPtr hDc);

        [DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
        public static extern int DeleteDC(IntPtr hdc);

        [DllImport("gdi32.dll", ExactSpelling = true)]
        public static extern IntPtr SelectObject(IntPtr hDc, IntPtr hObject);

        [DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
        public static extern int DeleteObject(IntPtr hObject);
    }

public class Win32Utilities
    {
        public static void SetBackground(Control control, Bitmap bitmap, byte opacity = 255)
        {
            if (bitmap.PixelFormat != PixelFormat.Format32bppArgb)
                throw new ApplicationException("Please use the 32ppp image with alpha-channel");

            var screenDc = NativeMethods.GetDC(IntPtr.Zero);
            var memoryDc = NativeMethods.CreateCompatibleDC(screenDc);
            var hBitmap = IntPtr.Zero;
            var oldBitmap = IntPtr.Zero;
            try
            {
                hBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
                oldBitmap = NativeMethods.SelectObject(memoryDc, hBitmap);

                var size = new NativeMethods.Size(bitmap.Width, bitmap.Height);
                var pointSource = new NativeMethods.Point(0, 0);
                var topPos = new NativeMethods.Point(control.Left, control.Top);
                var blend = new NativeMethods.BlendFunction
                {
                    BlendOp = 0,
                    BlendFlags = 0,
                    SourceConstantAlpha = opacity,
                    AlphaFormat = 1
                };

                NativeMethods.UpdateLayeredWindow(control.Handle, screenDc, ref topPos, ref size, memoryDc, ref pointSource, 0, ref blend, 2);
            }
            finally
            {
                NativeMethods.ReleaseDC(IntPtr.Zero, screenDc);
                if (hBitmap != IntPtr.Zero)
                {
                    NativeMethods.SelectObject(memoryDc, oldBitmap);
                    NativeMethods.DeleteObject(hBitmap);
                }
                NativeMethods.DeleteDC(memoryDc);
            }
        }

    }

    public class SplashScreenForm : ComForm
    {
        #region Properties

        private Bitmap? _bitmap;
        public Bitmap? Bitmap {
            get => _bitmap;
            set {
                _bitmap = value;
                Win32Utilities.SetBackground(
                    this, 
                    value ?? throw new InvalidOperationException($"{nameof(Bitmap)} is null"), 
                    (byte)Opacity);
            }
        }

        private int _opacity = 255;
        public new int Opacity {
            get => _opacity;
            set {
                _opacity = value;
                Win32Utilities.SetBackground(
                    this, 
                    Bitmap ?? throw new InvalidOperationException($"{nameof(Bitmap)} is null"), 
                    (byte)value);
            }
        }

        // ExLayouts
        protected override CreateParams CreateParams {
            get {
                var parameters = base.CreateParams;
                if (!DesignMode)
                    parameters.ExStyle |= 0x00080000;

                return parameters;
            }
        }

        #endregion

        #region Constructors

        public SplashScreenForm()
        {
            FormBorderStyle = FormBorderStyle.None;
            TopMost = true;
            ShowInTaskbar = false;
        }

        #endregion

        #region Methods

        // Draggable
        protected override void WndProc(ref Message m)
        {
            const int wmNchittest = 0x84;
            const int htClient = 2;
            switch (m.Msg)
            {
                case wmNchittest:
                    m.Result = (IntPtr)htClient;
                    break;

                default:
                    base.WndProc(ref m);
                    break;
            }
        }

        #endregion
    }