jingwood / d2dlib

A .NET library for hardware-accelerated, high performance, immediate mode rendering via Direct2D.
MIT License
234 stars 40 forks source link

Several Issues #106

Closed FraserElectronics closed 1 year ago

FraserElectronics commented 1 year ago

Really, really keen to use d2dlib but am very quickly coming across a number of issues:

I have created a custom control that derives from D2DControl in OnRender I do:

g.FillRectangle( _componentBounds, D2DColor.Cyan );

if ( _showBorder )
{
    g.DrawRectangle( _componentBounds, D2DColor.Blue );
}

When I run it, the rectangle (both filled and the border) do not fill the control bounds. If I drag the window even one pixel it then fills the control area. If I print out this.Bounds, the position and size match the control area but d2dlib does not seem to use those dimensions. I intercept OnResize to keep _componentBounds up to date. In the screenshot below you can see the Cyan rectangle which is 436 pixels high but if I print _componentBounds in OnRender it shows {X=0,Y=0,Width=1069,Height=589}.

I also am having problems with drawing text, specifically: I use MeasureText to get the size of a string so that I can centre it using the following code: Note, I am not exactly sure what the last parameter should be - I have set it to the total size of the custom control - perhaps this could be the issue?

var textrect = g.MeasureText( _title, _titleFont.Name, _titleFont.Size, _componentBounds.Size );
var tr = new Rectangle( ( int )( ( _componentBounds.Width / 2 ) - ( textrect.Width / 2 ) ), ( int )textrect.height, ( int )textrect.width, ( int )textrect.height );
g.DrawTextCenter( _title, D2DColor.Black, _titleFont.Name, _titleFont.Size, tr );

using ( var p = Device.CreatePen( D2DColor.Red ) )
{ 
    g.DrawRectangle( tr, p );
}

The text appears split across two lines (I draw the rectangle using the parameters for MeasureText and it looks like the text should fit.

Also, the quality of the text is poor at smaller font sizes - it is not smooth at all.

Please see the attached screenshot that shows these issues:

d2dlib

Sorry for the rambling :-(

Andy

jingwood commented 1 year ago

It maybe a DPI problem, you may change the DPI before the rendering.

g.SetDPI(96, 96);

Try change the value to 72 or 120 and check the result.

Usually if you are using a high resolution display and your .NET program not be set to support high resolution or the settings are not matched, you might get the a problem like this. See document like this.

FraserElectronics commented 1 year ago

Okay, that does have an effect - it is strange though, when I first run my application (first screenshot) you can see a comparison with the same string in the same font, size and style drawn in a PictureBox control. Then if I resize the window, the d2d control redraws and the text looks a lot better - very strange. It seems that if I use g.SetDPI( 128,128) the two strings seem to closely match in size. FYI, I am running on a 2K monitor with Windows scaling set to 100%.

Screenshot 1

d2dlib_1

Screenshot 2

d2dlib_2

Andy

FraserElectronics commented 1 year ago

Sorry to be a pain but still struggling with this :-( This is my simple custom control code:

    public class CustomD2D : D2DControl
    {
        public CustomD2D()
        {
            _componentBounds = new Rectangle();
            this.ShowFPS = true;
        }

        protected override void OnResize( EventArgs e )
        {
            _componentBounds.Width = this.ClientRectangle.Width - 1;
            _componentBounds.Height = this.ClientRectangle.Height - 1;

            Invalidate();
        }

        protected override void OnSizeChanged( EventArgs e )
        {
            _componentBounds.Width = this.ClientRectangle.Width - 1;
            _componentBounds.Height = this.ClientRectangle.Height - 1;

            Invalidate();
        }

        protected override void OnRender( D2DGraphics g )
        {
            g.Antialias = true;

            //g.SetDPI( 123, 164 );
            g.FillRectangle( _componentBounds, D2DColor.Cyan );
            g.DrawRectangle( _componentBounds, D2DColor.Blue );
            g.DrawLine( _componentBounds.Left, _componentBounds.Top, _componentBounds.Right, _componentBounds.Bottom, D2DColor.Green );
            g.DrawLine( _componentBounds.Right, _componentBounds.Top, _componentBounds.Left, _componentBounds.Bottom, D2DColor.Green );

            var elapsed = DateTime.Now.Subtract( _lastRender );
            using ( var font = new Font( "Arial", 22 ) )
            {
                string s = $"{_componentBounds.ToString()}\r\n{elapsed.TotalMilliseconds.ToString()}";
                g.DrawText( s, D2DColor.Black, font, _componentBounds.Left + 20, _componentBounds.Top + 20 );
            }

            _lastRender = DateTime.Now;
        }

        /// <summary>
        /// Holds the bounding rectangle of the chart control.
        /// </summary>
        protected Rectangle _componentBounds;
        private DateTime _lastRender = DateTime.Now;    
    }

If I set the background of this control to Orange and dock it as Fill in a WinForms application and run it I get:

1

You can see that _componentBounds holds the correct size but the graphics are not rendering accordingly. If I drag the main WinForms window the control resizes perfectly.

2

I found that by playing around with SetDPI that I could get the rectangle to fill the control area perfectly.

g.SetDPI( 123, 164 );

As you can see:

3

But as soon as I resize the main window, it all goes wrong :-(

4

It is also odd that when the application first runs, nothing is anti-aliased (see the text and lines) but as soon as I resize the window the text and lines become anti-aliased. I should also point out that I call Invalidate on the custom control from a timer in the main application once per millisecond so it is not subsequent calls to OnRender that is changing the functionality, it is something to do with resizing the client area ?

I would appreciate any guidance or comments.

Andy

MarkhusGenferei commented 1 year ago

In the example below, [A] and [B] do the job for the first run and when the control is resized.

Don't hardcode DPI. Instead Configure your app for high DPI support (read this). Then, as in the example below, [C] de-scales the rectangle given by Windows before drawing.

Markhus

public class TryD2DControl : D2DControl
    {
        public TryD2DControl() : base()
        {
            // [A] ask to redraw this control when resized
            this.ResizeRedraw = true;       
        }

        /// <summary>
        ///  [B] resize the device when the control is resized
        /// </summary>
        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);
            this.Device.Resize();
        }

        /// <summary>
        /// Return the given control's DPI factor.
        /// For example, for a 120 DPI, if multiply (1.25, 1.25), else (0.8, 0.8) 
        /// </summary>
        public D2DSize GetDPIScale(Control ctrl, bool multiply)
        {
            D2DSize dpiScale = new D2DSize(1f, 1f);

            Graphics g = ctrl.CreateGraphics();

            try
            {
                //int dpi = ctrl.DeviceDpi;   /// .net >= 4.8 required
                dpiScale.width = g.DpiX / 96f;
                dpiScale.height = g.DpiY / 96f;

                if (!multiply)
                {
                    dpiScale.width = 1f / dpiScale.width;
                    dpiScale.height = 1f / dpiScale.height;
                }
            }
            finally
            {
                g.Dispose();
            }

            return dpiScale;
        }

        protected override void OnRender(D2DGraphics g)
        {
            // all coordinates and sizes reported by Windows are DPI scaled
            D2DRect rect = this.ClientRectangle;

            // [C] de-scale client rectangle
            D2DSize dpiScale = GetDPIScale(this, false);
            rect.Width *= dpiScale.width;
            rect.Height *= dpiScale.height;

            g.FillRectangle(rect, D2DColor.Blue);
            g.DrawRectangle(rect, D2DColor.Chocolate);

            g.DrawLine(new D2DPoint(rect.left, rect.top), new D2DPoint(rect.right, rect.bottom), D2DColor.Chocolate);
            g.DrawLine(new D2DPoint(rect.right, rect.top), new D2DPoint(rect.left, rect.bottom), D2DColor.Chocolate);

            Rectangle r = (Rectangle) rect;
            g.DrawText(r.ToString(), D2DColor.WhiteSmoke, 10, 10);
        }
    }
jingwood commented 1 year ago

Thanks @MarkhusGenferei!

I have tested this, as @FraserElectronics said, seems the D2DControl can't calculate the DPI correctly during initialization.

As a workaround, by calling the Reisze method will resolve this.

public partial class MyControl : D2DControl
{
    public MyControl()
    {
    }

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        this.Device.Resize();      // call Resize during OnResize
    }

    protected override void OnRender(D2DGraphics g)
    {
        // get correct size
        g.DrawRectangle(this.ClientRectangle, D2DColor.DarkOrange);

        base.OnRender(g);
    }
}

Try to fix this in next versions.

FraserElectronics commented 1 year ago

Thanks guys - much appreciated 👍

Andy

FraserElectronics commented 1 year ago

Have tested and it seems to work perfectly 😁 I will close the issue.