ScrollViewer is not working in WPF WindowsFormHost

不问归期 提交于 2019-11-30 05:12:23

Finally got the solution

Create this Class in your solution for above problem, and take new class control (ScrollViewerWindowsFormsHost) instead of WindowsFormsHost

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows.Forms.Integration;
using System.Windows.Media;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;

namespace WPFRichTextBox
{
class ScrollViewerWindowsFormsHost: WindowsFormsHost
{

    protected override void OnWindowPositionChanged(Rect rcBoundingBox)
    {
        base.OnWindowPositionChanged(rcBoundingBox);

        if (ParentScrollViewer == null)
            return;

        GeneralTransform tr = ParentScrollViewer.TransformToAncestor(MainWindow);
        var scrollRect = new Rect(new Size(ParentScrollViewer.ViewportWidth, ParentScrollViewer.ViewportHeight));
        scrollRect = tr.TransformBounds(scrollRect);

        var intersect = Rect.Intersect(scrollRect, rcBoundingBox);
        if (!intersect.IsEmpty)
        {
            tr = MainWindow.TransformToDescendant(this);
            intersect = tr.TransformBounds(intersect);
        }

        SetRegion(intersect);
    }

    protected override void OnVisualParentChanged(DependencyObject oldParent)
    {
        base.OnVisualParentChanged(oldParent);
        ParentScrollViewer = null;

        var p = Parent as FrameworkElement;
        while (p != null)
        {
            if (p is ScrollViewer)
            {
                ParentScrollViewer = (ScrollViewer)p;
                break;
            }

            p = p.Parent as FrameworkElement;
        }
    }

    private void SetRegion(Rect intersect)
    {
        using (var graphics = System.Drawing.Graphics.FromHwnd(Handle))
            SetWindowRgn(Handle, (new System.Drawing.Region(ConvertRect(intersect))).GetHrgn(graphics), true);
    }

    static System.Drawing.RectangleF ConvertRect(Rect r)
    {
        return new System.Drawing.RectangleF((float)r.X, (float)r.Y, (float)r.Width, (float)r.Height);
    }

    private Window _mainWindow;
    Window MainWindow
    {
        get
        {
            if (_mainWindow == null)
                _mainWindow = Window.GetWindow(this);

            return _mainWindow;
        }
    }

    ScrollViewer ParentScrollViewer { get; set; }

    [DllImport("User32.dll", SetLastError = true)]
    public static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);
}

}

XAML Code:

<Window x:Class="WPFRichTextBox.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
    xmlns:swfh="clr-namespace:WPFRichTextBox"
    Title="MainWindow" Height="600" Width="800" Background="LightBlue">
<Grid Loaded="Grid_Loaded">

    <ScrollViewer  Background="DarkOrange" VerticalScrollBarVisibility="Auto" Height="100"  Margin="11,160,12,301" Width="756" Name="scrollViewer1">
        <Canvas Height="200" Name="canvas1" Width="auto" >
      <swfh:ScrollableWindowsFormsHost ClipToBounds="True" Height="194" Width="715" Margin="10,5,0,0" Background="Gray">
                <wf:RichTextBox BackColor="Cornsilk" Text="RichTextBox" x:Name="richTbTest" BorderStyle="None" Enabled="True" ForeColor="Black" Width="550" Multiline="True" ReadOnly="True" />
      </swfh:ScrollableWindowsFormsHost>
        </Canvas>
    </ScrollViewer>
</Grid>

Just in case someone else has my edge case, where I have a WinForms UserControl hosted inside a WPF UserControl, which itself is hosted inside a WinForms Form (don't ask...) - the class Avinash provided didn't fix my clipping issues.

But there was a modified version on a forum thread somewhere, which did the trick - so I thought I'd post it here for ease.

class WindowsFormsHostEx : WindowsFormsHost
{
    private PresentationSource _presentationSource;

    public WindowsFormsHostEx()
    {
        PresentationSource.AddSourceChangedHandler(this, SourceChangedEventHandler);
    }

    protected override void OnWindowPositionChanged(Rect rcBoundingBox)
    {
        base.OnWindowPositionChanged(rcBoundingBox);

        if (ParentScrollViewer == null)
            return;

        GeneralTransform tr = RootVisual.TransformToDescendant(ParentScrollViewer);
        var scrollRect = new Rect(new Size(ParentScrollViewer.ViewportWidth, ParentScrollViewer.ViewportHeight));

        var intersect = Rect.Intersect(scrollRect, tr.TransformBounds(rcBoundingBox));
        if (!intersect.IsEmpty)
        {
            tr = ParentScrollViewer.TransformToDescendant(this);
            intersect = tr.TransformBounds(intersect);
        }
        else
            intersect = new Rect();

        int x1 = (int)Math.Round(intersect.Left);
        int y1 = (int)Math.Round(intersect.Top);
        int x2 = (int)Math.Round(intersect.Right);
        int y2 = (int)Math.Round(intersect.Bottom);

        SetRegion(x1, y1, x2, y2);
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);

        if (disposing)
            PresentationSource.RemoveSourceChangedHandler(this, SourceChangedEventHandler);
    }

    private void SourceChangedEventHandler(Object sender, SourceChangedEventArgs e)
    {
        ParentScrollViewer = FindParentScrollViewer();
    }

    private ScrollViewer FindParentScrollViewer()
    {
        DependencyObject vParent = this;
        ScrollViewer parentScroll = null;
        while (vParent != null)
        {
            parentScroll = vParent as ScrollViewer;
            if (parentScroll != null)
                break;

            vParent = LogicalTreeHelper.GetParent(vParent);
        }
        return parentScroll;
    }

    private void SetRegion(int x1, int y1, int x2, int y2)
    {
        SetWindowRgn(Handle, CreateRectRgn(x1, y1, x2, y2), true);
    }

    private Visual RootVisual
    {
        get
        {
            if (_presentationSource == null)
                _presentationSource = PresentationSource.FromVisual(this);

            return _presentationSource.RootVisual;
        }
    }

    private ScrollViewer ParentScrollViewer { get; set; }

    [DllImport("User32.dll", SetLastError = true)]
    static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);

    [DllImport("gdi32.dll")]
    static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);
}

We are using multiple ScrollViewers and also a ViewBox so none of the mentioned solutions worked for us. So here is our soloution which can also deal with different DPI settings.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms.Integration;
using System.Windows.Media;
using System.Windows.Threading;


namespace XYZ
{

    public class ClippingWindowsFormsHost : WindowsFormsHost
    {
        private readonly DispatcherTimer _updateTimer;

        private Rect _bounds;

        private PresentationSource _source;

        public ClippingWindowsFormsHost()
        {
            PresentationSource.AddSourceChangedHandler(this, _sourceChangedEventHandler);

            _updateTimer = new DispatcherTimer(DispatcherPriority.Render);
            _updateTimer.Tick += _updateTick;
            _updateTimer.Interval = TimeSpan.FromMilliseconds(100);
        }

        private void _updateTick(object sender, EventArgs e)
        {
            _updateTimer.Stop();

            if (_source == null)
                return;


            // Get the Rect of the scrollviewer on screen.
            Rect scrollRect = _getScrollRect();

            // apply dpi settings
            scrollRect = _scaleDpi(scrollRect);

            if (scrollRect.Width > 0 && scrollRect.Height > 0) // if the rect is valid...
            {
                int x1 = (int) Math.Ceiling(scrollRect.X);
                int y1 = (int) Math.Ceiling(scrollRect.Y);
                int x2 = (int) Math.Ceiling(scrollRect.Right);
                int y2 = (int) Math.Ceiling(scrollRect.Bottom);

                SetWindowRgn(Handle, CreateRectRgn(x1, y1, x2, y2), true);
            }
            else
                SetWindowRgn(Handle, CreateRectRgn(0, 0, 0, 0), true);

        }

        private Rect _scaleDpi(Rect rect)
        {
            if (_source.CompositionTarget != null)
            {
                Matrix transformToDevice = _source.CompositionTarget.TransformToDevice;
                if (!transformToDevice.IsIdentity)
                {
                    Point scaledSize = transformToDevice.Transform(new Point(rect.Width, rect.Height));
                    rect = new Rect(rect.X, rect.Y, scaledSize.X, scaledSize.Y);
                }
            }

            return rect;
        }

        [DllImport("User32.dll", SetLastError = true)]
        private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);

        [DllImport("gdi32.dll")]
        private static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);

        protected override void OnWindowPositionChanged(Rect rcBoundingBox)
        {
            base.OnWindowPositionChanged(rcBoundingBox);
            _updateClipping(rcBoundingBox);
        }

        private void _updateClipping(Rect bounds)
        {
            if (_source == null || _bounds == bounds)
                return;

            _bounds = bounds;

            // Only update clipping in certain intervals, otherwise splitpanels can create huge cpu load
            _updateTimer.Stop();
            _updateTimer.Start();
        }

        private Rect _getScrollRect()
        {
            ScrollViewer scrollViewer = _getTopScrollViewer();

            // Get the screenposition of the scrollviewer
            Point topLeft = scrollViewer.PointToScreen(new Point(0, 0));
            Point bottomRight = scrollViewer.PointToScreen(new Point(scrollViewer.ViewportWidth, scrollViewer.ViewportHeight));

            Rect scrollRect = new Rect(topLeft, bottomRight);



            // Get "this" position and use it to offset the scrollrect
            // because that is basically the scrolled distance
            Point myPosition = PointToScreen(new Point());
            scrollRect.Offset(-myPosition.X, -myPosition.Y);

            return scrollRect;
        }

        private ScrollViewer _getTopScrollViewer()
        {
            DependencyObject parent = this;
            ScrollViewer lastViewer = null;
            while ((parent = VisualTreeHelper.GetParent(parent)) != null)
            {
                ScrollViewer viewer = parent as ScrollViewer;
                if (viewer != null)
                    lastViewer = viewer;
            }

            return lastViewer;
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);

            if (disposing)
            {
                _updateTimer.Stop();
                PresentationSource.RemoveSourceChangedHandler(this, _sourceChangedEventHandler);
            }
        }

        private void _sourceChangedEventHandler(object sender, SourceChangedEventArgs e)
        {
            _updateTimer.Stop();
            _source = e.NewSource;
        }
    }
}

I found Marlon's answer to be the best, however it did not work at all if the user had a different DPI setting. This is Marlon's answer, but solved to scale for DPI. I also added a location changed event since I needed to move a popup that was on top of the WindowsFormsHost content in tandem with the WindowsFormsHost in the scrollviewer.

#region Using Declarations

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms.Integration;
using System.Windows.Media;

#endregion

public class WindowsFormsHostEx : WindowsFormsHost
{
    #region DllImports
    [DllImport("User32.dll", SetLastError = true)]
    static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);

    [DllImport("gdi32.dll")]
    static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);

    #endregion

    #region Events
    public event EventHandler LocationChanged;
    #endregion

    #region Members
    private PresentationSource _presentationSource;
    #endregion

    #region Properties
    private ScrollViewer ParentScrollViewer { get; set; }
    private bool Scrolling { get; set; }
    public bool Resizing { get; set; }
    private Visual RootVisual
    {
        get
        {
            _presentationSource = PresentationSource.FromVisual(this);
            return _presentationSource.RootVisual;
        }
    }
    #endregion

    #region Constructors
    public WindowsFormsHostEx()
    {
        PresentationSource.AddSourceChangedHandler(this, SourceChangedEventHandler);
    }
    #endregion

    #region Methods

    protected override void OnWindowPositionChanged(Rect rcBoundingBox)
    {
        DpiScale dpiScale = VisualTreeHelper.GetDpi(this);

        base.OnWindowPositionChanged(rcBoundingBox);

        Rect newRect = ScaleRectDownFromDPI(rcBoundingBox, dpiScale);
        Rect finalRect;
        if (ParentScrollViewer != null)
        {
            ParentScrollViewer.ScrollChanged += ParentScrollViewer_ScrollChanged;
            ParentScrollViewer.SizeChanged += ParentScrollViewer_SizeChanged;
            ParentScrollViewer.Loaded += ParentScrollViewer_Loaded;
        }

        if (Scrolling || Resizing)
        {
            if (ParentScrollViewer == null)
                return;
            MatrixTransform tr = RootVisual.TransformToDescendant(ParentScrollViewer) as MatrixTransform;

            var scrollRect = new Rect(new Size(ParentScrollViewer.ViewportWidth, ParentScrollViewer.ViewportHeight));
            var c = tr.TransformBounds(newRect);

            var intersect = Rect.Intersect(scrollRect, c);
            if (!intersect.IsEmpty)
            {
                tr = ParentScrollViewer.TransformToDescendant(this) as MatrixTransform;
                intersect = tr.TransformBounds(intersect);
                finalRect = ScaleRectUpToDPI(intersect, dpiScale);
            }
            else
                finalRect = intersect = new Rect();

            int x1 = (int)Math.Round(finalRect.X);
            int y1 = (int)Math.Round(finalRect.Y);
            int x2 = (int)Math.Round(finalRect.Right);
            int y2 = (int)Math.Round(finalRect.Bottom);

            SetRegion(x1, y1, x2, y2);
            this.Scrolling = false;
            this.Resizing = false;

        }
        LocationChanged?.Invoke(this, new EventArgs());
    }

    private void ParentScrollViewer_Loaded(object sender, RoutedEventArgs e)
    {
        this.Resizing = true;
    }

    private void ParentScrollViewer_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        this.Resizing = true;
    }

    private void ParentScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (e.VerticalChange != 0 || e.HorizontalChange != 0 || e.ExtentHeightChange != 0 || e.ExtentWidthChange != 0)
            Scrolling = true;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);

        if (disposing)
        {
            PresentationSource.RemoveSourceChangedHandler(this, SourceChangedEventHandler);
            _presentationSource = null;
        }
    }

    private void SourceChangedEventHandler(Object sender, SourceChangedEventArgs e)
    {
        if (ParentScrollViewer != null)
        {
            ParentScrollViewer.ScrollChanged -= ParentScrollViewer_ScrollChanged;
            ParentScrollViewer.SizeChanged -= ParentScrollViewer_SizeChanged;
            ParentScrollViewer.Loaded -= ParentScrollViewer_Loaded;
        }
        ParentScrollViewer = FindParentScrollViewer();
    }

    private ScrollViewer FindParentScrollViewer()
    {
        DependencyObject vParent = this;
        ScrollViewer parentScroll = null;
        while (vParent != null)
        {
            parentScroll = vParent as ScrollViewer;
            if (parentScroll != null)
                break;

            vParent = LogicalTreeHelper.GetParent(vParent);
        }
        return parentScroll;
    }

    private void SetRegion(int x1, int y1, int x2, int y2)
    {
        SetWindowRgn(Handle, CreateRectRgn(x1, y1, x2, y2), true);
    }

    public static  Rect ScaleRectDownFromDPI(Rect _sourceRect, DpiScale dpiScale)
    {
        double dpiX = dpiScale.DpiScaleX;
        double dpiY = dpiScale.DpiScaleY;
        return new Rect(new Point(_sourceRect.X / dpiX, _sourceRect.Y / dpiY), new System.Windows.Size(_sourceRect.Width / dpiX, _sourceRect.Height / dpiY));
    }

    public static Rect ScaleRectUpToDPI(Rect _toScaleUp, DpiScale dpiScale)
    {
        double dpiX = dpiScale.DpiScaleX;
        double dpiY = dpiScale.DpiScaleY;
        return new Rect(new Point(_toScaleUp.X * dpiX, _toScaleUp.Y * dpiY), new System.Windows.Size(_toScaleUp.Width * dpiX, _toScaleUp.Height * dpiY));
    }
    #endregion
}
VivekDev

If your WindowsFormsHost is to be placed inside a UserControl, then the answer presented by Avinash may not work. So I had to tweak the ScrollViewerWindowsFormsHost class as follows.

    public class ScrollViewerWindowsFormsHost : WindowsFormsHost
    {
        protected override void OnWindowPositionChanged(Rect rcBoundingBox)
        {
            base.OnWindowPositionChanged(rcBoundingBox);

            if (ParentScrollViewer == null)
                //return; // Instead, you set the ParentScrollViewr by calling the following method.
                SetParentScrollViewer();

            GeneralTransform tr = ParentScrollViewer.TransformToAncestor(MainWindow);

            var scrollRect = new Rect(new Size(ParentScrollViewer.ViewportWidth, ParentScrollViewer.ViewportHeight));
            scrollRect = tr.TransformBounds(scrollRect);

            var intersect = Rect.Intersect(scrollRect, rcBoundingBox);
            if (!intersect.IsEmpty)
            {
                tr = MainWindow.TransformToDescendant(this);
                intersect = tr.TransformBounds(intersect);
            }

            SetRegion(intersect);
        }

        // This is new a new method. This is called from the above method.
        private void SetParentScrollViewer()
        {
            if (ParentScrollViewer is ScrollViewer)
                return; // that means its already set;

            var p = Parent as FrameworkElement;
            while (p != null)
            {
                if (p is ScrollViewer)
                {
                    ParentScrollViewer = (ScrollViewer)p;
                    break;
                }

                p = p.Parent as FrameworkElement;
            }
        }
        // Just comment out this method, you dont need this any more. You set the parent Scroll Viewer by calling SetParentScrollViewer Method.
        //protected override void OnVisualParentChanged(DependencyObject oldParent)
        //{
        //    base.OnVisualParentChanged(oldParent);
        //    ParentScrollViewer = null;

        //    var p = Parent as FrameworkElement;
        //    while (p != null)
        //    {
        //        if (p is ScrollViewer)
        //        {
        //            ParentScrollViewer = (ScrollViewer)p;
        //            break;
        //        }

        //        p = p.Parent as FrameworkElement;

        //    }
        //}

        private void SetRegion(Rect intersect)
        {
            using (var graphics = System.Drawing.Graphics.FromHwnd(Handle))
                SetWindowRgn(Handle, (new System.Drawing.Region(ConvertRect(intersect))).GetHrgn(graphics), true);
        }

        static System.Drawing.RectangleF ConvertRect(Rect r)
        {
            return new System.Drawing.RectangleF((float)r.X, (float)r.Y, (float)r.Width, (float)r.Height);
        }

        private Window _mainWindow;
        Window MainWindow
        {
            get
            {
                if (_mainWindow == null)
                    _mainWindow = Window.GetWindow(this);

                return _mainWindow;
            }
        }

        ScrollViewer ParentScrollViewer { get; set; }

        [DllImport("User32.dll", SetLastError = true)]
        public static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);
    }

Thats it. Every thing remains the same.

That's because ScrollViewer does not know it has to scroll. If your mouse is on RichTextBox, it will intercept all keys. You can subclass the RichTextBox(namely WndProc) and listen for mousewheel events and then send them to scrollViewer using RaiseEvent. Don't forget that WndProc runs on a seperate thread than WPF so you need to do something like:

case WM_MOUSEWHEEL: Dispatcher.BeginInvoke(new Action(() => VisualHelper.FindParent(richTextBox).RaiseEvent(..mouse wheel event with correct parameters..));

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!