2

I'm building custom user control that will be used to display tiles map, as base class I've chosen ScrollableControl, because I want to have scrollbars in my control.

I've successfully created paint logic that is responsible for painting only needed elements.

Now I'm trying to add static text that will be always visible in same place (in my case white box with red text in top left corner):

enter image description here

This isn't clearly visible on above gif, but that white box blinks and jumps a bit when I scroll using mouse or scrollbars.

My question is how should I change my code to have scrollable content and fixed position content on top of that scrollable content?

Is ScrollableControl good choice as base class?

Below is my code:

class TestControl : ScrollableControl
{
    private int _tileWidth = 40;
    private int _tileHeight = 40;
    private int _tilesX = 20;
    private int _tilesY = 20;

    public TestControl()
    {
        SetStyle(ControlStyles.ResizeRedraw, true);
        SetStyle(ControlStyles.UserPaint, true);
        SetStyle(ControlStyles.AllPaintingInWmPaint, true);
        SetStyle(ControlStyles.Opaque, true);
        SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
        UpdateStyles();
        ResizeRedraw = true;
        AutoScrollMinSize = new Size(_tilesX * _tileWidth, _tilesY * _tileHeight);

        Scroll += (sender, args) => { Invalidate(); };
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        e.Graphics.FillRectangle(new SolidBrush(BackColor), ClientRectangle);
        e.Graphics.TranslateTransform(AutoScrollPosition.X, AutoScrollPosition.Y);
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

        var offsetX = (AutoScrollPosition.X * -1) / _tileWidth;
        var offsetY = (AutoScrollPosition.Y * -1) / _tileHeight;

        var visibleX = Width / _tileWidth + 2;
        var visibleY = Height / _tileHeight + 2;

        var x = Math.Min(visibleX + offsetX, _tilesX);
        var y = Math.Min(visibleY + offsetY, _tilesY);

        for (var i = offsetX; i < x; i++)
        {
            for (var j = offsetY; j < y; j++)
            {
                e.Graphics.FillRectangle(Brushes.Beige, new Rectangle(i*_tileWidth, j*_tileHeight, _tileWidth, _tileHeight));
                e.Graphics.DrawString(string.Format("{0}:{1}", i, j), Font, Brushes.Black, new Rectangle(i * _tileWidth, j * _tileHeight, _tileWidth, _tileHeight));
            }
        }

        using (var p = new Pen(Color.Black))
        {
            for (var i = offsetX + 1; i < x; i++)
            {
                e.Graphics.DrawLine(p, i*_tileWidth, 0, i*_tileWidth, y*_tileHeight);
            }

            for (var i = offsetY + 1; i < y; i++)
            {
                e.Graphics.DrawLine(p, 0, i*_tileHeight, x*_tileWidth, i*_tileHeight);
            }
        }

        e.Graphics.FillRectangle(Brushes.White, AutoScrollPosition.X * -1, AutoScrollPosition.Y * -1, 35, 14);
        e.Graphics.DrawString("TEST", DefaultFont, new SolidBrush(Color.Red), AutoScrollPosition.X * -1, AutoScrollPosition.Y * -1);
    }
}

EDIT:
I've searched a bit and found UserControl that has similar functionality - https://www.codeproject.com/Articles/16009/A-Much-Easier-to-Use-ListView and after reading a bit more on control's author blog http://objectlistview.sourceforge.net/cs/blog1.html#blog-overlays I found out that he is using Transparent Form that is positioned on top of control. I really would like to avoid that, but still have overlay on top of my control.

Community
  • 1
  • 1
Misiu
  • 4,738
  • 21
  • 94
  • 198

2 Answers2

4

You are doing battle with a Windows system option named "Show window content while dragging". Always turned on by default, this web page shows how to turn it off.

Solves the problem, but it is not something you can rely on since it affects all scrollable window in all apps. Demanding that the user turns it off for you is unrealistic, users like this option so they'll just ignore you. That they did not provide an option to turn it off for a specific window was a pretty major oversight. It is an okay solution in a kiosk app.

Briefly, the way the option works is that Windows itself scrolls the window content with the ScrollWindowEx() winapi function. Using a bitblt of the window content to move pixels and only generating a paint request for the part of the window that was revealed by the scroll. Usually only a few lines of pixels, so completes very fast. Problem is, that bitblt moves your fixed pixels as well. The repaint moves them back. Pretty noticeable, the human eye is very sensitive to motion like that, helped avoid being lion lunch for the past million years.

You'll have to take the sting out of ScrollWindowsEx(), preventing it from moving pixels even though you can't stop it from being called. That takes a heavy sledgehammer, LockWindowUpdate(). You'll find code in this post.

using System.Runtime.InteropServices;
...

    protected override void OnScroll(ScrollEventArgs e) {
        if (e.Type == ScrollEventType.First) {
            LockWindowUpdate(this.Handle);
        }
        else {
            LockWindowUpdate(IntPtr.Zero);
            this.Update();
            if (e.Type != ScrollEventType.Last) LockWindowUpdate(this.Handle);
        }
    }

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool LockWindowUpdate(IntPtr hWnd);

Not that pretty, using a separate Label control ought to start sounding attractive.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Thank You so much for reply. Again Your code helped (as always)! Now when I use scrollbars I get smooth scroll and my label is at top. Unfortunately when I scroll using mouse wheel nothing changed, my white box still jumps. Can scrolling using mouse wheel be fixed too? – Misiu Apr 11 '17 at 06:19
  • I've add override for `OnMouseWheel` with: `LockWindowUpdate(Handle);base.OnMouseWheel(e);LockWindowUpdate(IntPtr.Zero);Update();` This solved my problem with mouse wheel. Please check this and update Your answer. This way someone having same problem will have working solution. – Misiu Apr 11 '17 at 12:24
  • I've noticed that when inside `OnScroll` `e.Type`==`ThumbPosition` I get weird behavior when resizing control (of entire form) http://i63.tinypic.com/2zfikbr.gif I've managed to fix this by changing `if (e.Type != ScrollEventType.Last) LockWindowUpdate(this.Handle);` to `if (e.Type != ScrollEventType.Last && e.Type != ScrollEventType.ThumbPosition) LockWindowUpdate(Handle);` Could You please take a look at this again and update Your answer? – Misiu Apr 12 '17 at 14:04
  • Good one, @Misiu. And one more fix. You also need to handle mouse wheel. protected override void OnMouseWheel(MouseEventArgs e) { LockWindowUpdate(Handle); base.OnMouseWheel(e); LockWindowUpdate(IntPtr.Zero); } – Tomaz Stih Jul 27 '17 at 12:47
0

can you just add a label to that control(on top), in other words - cant you use it as panel?