You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何绑定WPF托管的UWP InkCanvas中的InkStrokes?

WPF托管UWP InkCanvas并与Elmish.WPF绑定InkStrokes解决方案

问题描述

我是UWP/WinRT新手,目前在WPF窗口中托管自定义WinRT InkCanvas,希望通过WPF窗口的XAML绑定将InkStrokes数据与后端F#模块(Elmish.wpf)关联,但不知如何获取/设置InkContainer中的InkStrokes。当前Elmish.wpf与WinRT InkCanvas已在同一应用中正常运行,以下是已实现的代码:

已实现代码

1. WPF容器——ProgressNoteWindow

<Window x:Class="Stargate.XI.Client.Views.ProgressNoteWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:xaml="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost"
        xmlns:controls="clr-namespace:Microsoft.Toolkit.Wpf.UI.Controls;assembly=Microsoft.Toolkit.Wpf.UI.Controls"
        xmlns:local="clr-namespace:Stargate.XI.Client.Views"
        WindowStartupLocation="CenterScreen" WindowState="Maximized"
        mc:Ignorable="d"
        Title="ProgressNoteWindow" d:DesignHeight="1200" d:DesignWidth="1200">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <xaml:WindowsXamlHost x:Name="MyInkControl" Grid.Row="1" InitialTypeName="Loader2.MyUserControl2" ChildChanged="WindowsXamlHost_ChildChanged" />

        <Button Command="{Binding SaveCommand}" Content="Save"/>
    </Grid>

</Window>

2. 附加属性——InkCanvasBinder

public static class InkCanvasBinder
{
    public static InkStrokeContainer GetInkStrokes(DependencyObject obj) =>
        obj.GetValue(InkStrokesProperty) as InkStrokeContainer;

    public static void SetInkStrokes(DependencyObject obj, InkStrokeContainer value) =>
        obj.SetValue(InkStrokesProperty, value);

    public static DependencyProperty InkStrokesProperty = DependencyProperty.RegisterAttached(
        "InkStrokes", typeof(InkStrokeContainer), typeof(InkCanvasBinder),
        new PropertyMetadata(null, InkStrokesProperty_PropertyChanged));

    private static void InkStrokesProperty_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var inkCanvas = d as InkCanvas;
        if (inkCanvas != null) inkCanvas.InkPresenter.StrokeContainer = e.NewValue as InkStrokeContainer;
    }
}

3. UWP项目中的UserControl

<UserControl
    x:Class="Loader2.MyUserControl2"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Loader2"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="1200"
    d:DesignWidth="1200">

    <Grid Background="LightCoral">
        <InkCanvas  x:Name="TheInkCanvas" Loaded="InkCanvas_OnLoaded" local:InkCanvasBinder.InkStrokes = "{x:Bind MyInkStrokeContainer}"/>

        <!-- Ensure the InkToolbar is declared after the InkCanvas. If not, the InkCanvas overlay renders the InkToolbar inaccessible., Mode=OneWay-->
        <InkToolbar x:Name="myInkToolbar" TargetInkCanvas="{x:Bind TheInkCanvas}" Width="300" Height="50" Margin="10,10,10,10" HorizontalAlignment="Left" VerticalAlignment="Top" />
    </Grid>
    
</UserControl>

4. UWP项目中的UserControl代码后置

using Windows.UI.Core;
using Windows.UI.Input.Inking;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace Loader2
{
    public sealed partial class MyUserControl2 : UserControl
    {
        public MyUserControl2()
        {
            this.InitializeComponent();
        }

        private void InkCanvas_OnLoaded(object sender, RoutedEventArgs e)
        {
            TheInkCanvas.InkPresenter.InputDeviceTypes = CoreInputDeviceTypes.Mouse | CoreInputDeviceTypes.Touch;
        }

        public InkStrokeContainer MyInkStrokeContainer
        {
            get { return (InkStrokeContainer)GetValue(MyInkStrokeContainerProperty); }
            set { SetValue(MyInkStrokeContainerProperty, value); }
        }

        public static readonly DependencyProperty MyInkStrokeContainerProperty =
            DependencyProperty.Register("MyInkStrokeContainer", typeof(InkStrokeContainer), typeof(MyUserControl2), new PropertyMetadata(null));
    }
}

解决方案步骤

1. 获取托管的UWP控件实例

在ProgressNoteWindow的代码后置中,通过ChildChanged事件保存MyUserControl2的引用,方便后续访问:

private Loader2.MyUserControl2 _inkUserControl;

private void WindowsXamlHost_ChildChanged(object sender, EventArgs e)
{
    _inkUserControl = MyInkControl.Child as Loader2.MyUserControl2;
}

2. 在Elmish.WPF中同步数据

Elmish.WPF通过Model和Msg管理状态,你需要:

  • 在Model中添加存储InkStrokeContainer(或序列化后的字节流)的字段
  • 创建对应的Msg(比如SetInkStrokes InkStrokeContainer、SaveInkStrokes)
  • 在Update函数中处理这些Msg,更新Model状态

3. 获取InkStrokes用于保存

在Save命令的处理逻辑中,直接通过保存的控件引用获取当前笔触容器:

// 示例Elmish命令处理
let saveInkStrokesCmd (model: Model) =
    match model.InkUserControlRef with // 假设Model中存储了_inkUserControl的引用
    | Some uc ->
        async {
            let container = uc.MyInkStrokeContainer
            use stream = new MemoryStream()
            do! container.SaveAsync(stream.AsRandomAccessStream()) |> Async.AwaitTask
            stream.Position <- 0
            let bytes = stream.ToArray()
            // 调用后端API保存字节数据
            do! saveStrokesToBackend bytes
            return SavedSuccessfully
        } |> Cmd.ofAsyncMsg
    | None -> Cmd.none

4. 加载InkStrokes到画布

当需要从后端加载数据时,创建InkStrokeContainer并加载数据,然后赋值给UserControl的属性:

// 示例加载逻辑
let loadInkStrokesCmd () =
    async {
        let! bytes = getStrokesFromBackend()
        use stream = new MemoryStream(bytes)
        let container = new InkStrokeContainer()
        do! container.LoadAsync(stream.AsRandomAccessStream()) |> Async.AwaitTask
        return SetInkStrokes container
    } |> Cmd.ofAsyncMsg

// 在Update中处理SetInkStrokes Msg
let update msg model =
    match msg with
    | SetInkStrokes container ->
        match model.InkUserControlRef with
        | Some uc -> uc.MyInkStrokeContainer <- container
        | None -> ()
        { model with CurrentInkContainer = container }, Cmd.none
    | _ -> model, Cmd.none

5. 优化附加属性实现双向同步

当前的InkCanvasBinder只支持从属性到画布的单向同步,需要添加笔触变化的监听,反向更新绑定属性:

private static void InkStrokesProperty_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var inkCanvas = d as InkCanvas;
    if (inkCanvas == null) return;

    // 移除旧容器的事件监听
    if (e.OldValue != null)
    {
        inkCanvas.InkPresenter.StrokesCollected -= OnStrokesChanged;
        inkCanvas.InkPresenter.StrokesErased -= OnStrokesChanged;
    }

    inkCanvas.InkPresenter.StrokeContainer = e.NewValue as InkStrokeContainer;

    // 添加新容器的事件监听
    if (e.NewValue != null)
    {
        inkCanvas.InkPresenter.StrokesCollected += OnStrokesChanged;
        inkCanvas.InkPresenter.StrokesErased += OnStrokesChanged;
    }
}

private static void OnStrokesChanged(InkPresenter sender, object args)
{
    var inkCanvas = sender.Target as InkCanvas;
    if (inkCanvas != null)
    {
        // 通知属性变化,触发x:Bind更新UserControl的MyInkStrokeContainer
        SetInkStrokes(inkCanvas, sender.StrokeContainer);
    }
}

内容的提问来源于stack exchange,提问作者Alan Wayne

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 04:15:35