WPF中的ScrollViewer在WindowsFormHost中无法正常工作

13

在这里输入图片描述我在我的WPF表单中有一个带有RichTextBox的WindowsFormHost,我为该WindowsFormHost提供了ScrollViewer,但它不起作用,WindowsFormHost超出了ScrollViewer的范围...

我的XAML是...

<ScrollViewer Background="DarkOrange" VerticalScrollBarVisibility="Auto" Height="80" MaxHeight="85" Margin="11,243,12,218" Width="756">
        <Canvas Height="100" Name="canvas1" Width="auto" >
            <WindowsFormsHost ClipToBounds="True" Height="120" Width="715" Margin="10,5,0,0" Name="winHostTEst" Background="Gray">
                <wf:RichTextBox BackColor="Cornsilk" Text="RichTextBox" x:Name="richTbTest" BorderStyle="None" Enabled="True" ForeColor="Black" Width="550" Multiline="True" ReadOnly="True" />
            </WindowsFormsHost>
        </Canvas>
    </ScrollViewer>

以下是两个解决此问题的链接,但我无法实现。请您也查看这些链接并解决我的问题。

链接如下:

http://blogs.msdn.com/b/ryanvog/archive/2009/01/20/clipping-legacy-content-hosted-inside-a-wpf-scrolling-region.aspx

http://www.mycsharp.de/wbb2/thread.php?threadid=76625

提前致谢。

6个回答

14

终于找到了解决方案。

在你的解决方案中创建这个类,并使用新的类控件(ScrollViewerWindowsFormsHost)代替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 代码:

<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>

我在没有父级ScrollViewer的情况下使用了它,但它非常好并解决了我的问题。我想知道你为什么没有得到任何荣誉 :) - RoeeK
谢谢,这对我很有帮助。我知道我来晚了,但您可能想添加一种管理dpi因素的方法,以避免尺寸不正确。 - user1098580
2
我用Window.GetWindow(this)替换了对MainWindow的引用,但除此之外,我认为这非常优秀。谢谢! - ravuya
如果要将WindowsFormsHost放置在UserControl中,则此解答可能无法正常工作。请参见以下链接以获取修正方法。 - VivekDev
在我的情况下,我需要使用 VisualTreeHelper.GetParent() 来获取 ScrollContentPresenter。然后,您可以访问 ScrollContentPresenter.ScrollOwner 属性来获取 ScrollViewer。同样,似乎 @Marlon 也需要不同的方法来获取 ScrollViewer1 - Nicholas Miller

9

万一有其他人遇到了和我一样的特殊情况,我的情况是在一个WinForms用户控件中托管了一个WPF用户控件,而这个WPF用户控件本身又被托管在一个WinForms窗体中(别问我为什么……)——Avinash提供的类没有解决我的剪切问题。

但是在某个论坛帖子上有一个修改过的版本,它解决了我的问题——所以我想在这里发布它以方便大家。

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);
}

这有一个缺点 - 托管控件的滚动不如标准的WindowsFormsHost流畅。除此之外,它可以工作。 - Mikhail Orlov
这对我起作用了,而被接受的解决方案却没有。具体来说,我正在一个滚动区域中托管一个WinForms控件在一个WPF UserControl中。 - Zachary

4

我认为Marlon的答案是最好的,但是如果用户有不同的DPI设置,它根本不起作用。这是Marlon的答案,但已经解决了DPI缩放的问题。我还添加了一个位置更改事件,因为我需要将弹出窗口与滚动视图中的WindowsFormsHost内容一起移动。

#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
}

3
我们正在使用多个ScrollViewers和一个ViewBox,所以之前提到的解决方案都不适用于我们。以下是我们的解决方案,可以处理不同的DPI设置。
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;
        }
    }
}

1
如果您的WindowsFormsHost要放置在UserControl中,则 Avinash提供的答案可能无法正常工作。因此,我必须按照以下方式调整 ScrollViewerWindowsFormsHost 类。
    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);
    }

就是这样,一切保持不变。


0

这是因为ScrollViewer不知道它需要滚动。如果你的鼠标在RichTextBox上,它会拦截所有按键。你可以子类化RichTextBox(即WndProc),监听鼠标滚轮事件,然后使用RaiseEvent将它们发送到scrollViewer。不要忘记WndProc运行在一个与WPF不同的线程上,所以你需要做一些像这样的事情:

case WM_MOUSEWHEEL: Dispatcher.BeginInvoke(new Action(() => VisualHelper.FindParent(richTextBox).RaiseEvent(..带有正确参数的鼠标滚轮事件..));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接