WPF开发一个语音转文字输入软件(一)

本文探索的Demo地址:
https://gitee.com/lishuangquan1987/try_win32
https://github.com/lishuangquan1987/try_win32
后续会把他当做一个开源项目来维护

需求

开发一个软件,能够让用户说话来进行文字输入。具体如下:

  • 像腾讯电脑管家那样的悬浮球悬浮在其他程序之上,支持拖动,点击开始录音,再点击结束录音。有录音提示、忙碌提示。
  • 能够像输入法一样,对任何程序都具有语音输入功能,是一个通用的程序。

难点突破

  1. WPF 实现腾讯电脑管家悬浮球效果。
  2. WPF程序不抢占其他程序的焦点。
  3. WPF程序能够把语音翻译后的文字发送给具有焦点的程序。
  4. 语音转文字

突破过程

程序不具有焦点,且不抢占其他程序的焦点

winform的实现方式:

经过查资料,如下代码能达到要求:

public partial class Form2 : Form
{
    public Form2()
    {
        InitializeComponent();
        this.TopMost = true;
    }

    private const int WS_EX_TOOLWINDOW = 0x00000080;
    private const int WS_EX_NOACTIVATE = 0x08000000;

    private void button1_Click(object sender, EventArgs e)
    {
        this.textBox1.Text += "hello";//222211122
    }

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ExStyle |= (WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW);
            cp.Parent = IntPtr.Zero; // Keep this line only if you used UserControl
            return cp;
        }
    }
}

实现效果如下:(一边移动窗体,一边按键盘输入,输入的还是记事本,可见Form2完全无焦点)
在这里插入图片描述
如上代码可以看到,是重写了CreateParams属性,winform可以重写,那wpf呢?
在github上查看winform源码,发现最终这个CreateParams是调用win32的SetWindowLong函数来进行设置。
翻看源码如下:
https://github.com/dotnet/winforms/blob/656dc8153a0bc464063ef9a4112f2eb94dcd100f/src/System.Windows.Forms/src/System/Windows/Forms/Form.cs#L758

/// <summary>
    ///  Retrieves the CreateParams used to create the window.
    ///  If a subclass overrides this function, it must call the base implementation.
    /// </summary>
    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;

            if (IsHandleCreated && WindowStyle.HasFlag(WINDOW_STYLE.WS_DISABLED))
            {
                // Forms that are parent of a modal dialog must keep their WS_DISABLED style
                cp.Style |= (int)WINDOW_STYLE.WS_DISABLED;
            }
            else if (TopLevel)
            {
                // It doesn't seem to make sense to allow a top-level form to be disabled
                cp.Style &= ~(int)WINDOW_STYLE.WS_DISABLED;
            }

            if (TopLevel && (_formState[s_formStateLayered] != 0))
            {
                cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_LAYERED;
            }

            if (Properties.TryGetValue(s_propDialogOwner, out IWin32Window? dialogOwner))
            {
                cp.Parent = GetSafeHandle(dialogOwner).Handle;
            }

            FillInCreateParamsBorderStyles(cp);
            FillInCreateParamsWindowState(cp);
            FillInCreateParamsBorderIcons(cp);

            if (_formState[s_formStateTaskBar] != 0)
            {
                cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
            }

            FormBorderStyle borderStyle = FormBorderStyle;
            if (!ShowIcon && (borderStyle is FormBorderStyle.Sizable or FormBorderStyle.Fixed3D or FormBorderStyle.FixedSingle))
            {
                cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_DLGMODALFRAME;
            }

            if (IsMdiChild)
            {
                if (Visible && (WindowState is FormWindowState.Maximized or FormWindowState.Normal))
                {
                    Form? formMdiParent = Properties.GetValueOrDefault<Form>(s_propFormMdiParent);
                    Form? form = formMdiParent?.ActiveMdiChildInternal;

                    if (form is not null && form.WindowState == FormWindowState.Maximized)
                    {
                        cp.Style |= (int)WINDOW_STYLE.WS_MAXIMIZE;
                        _formState[s_formStateWindowState] = (int)FormWindowState.Maximized;
                        SetState(States.SizeLockedByOS, true);
                    }
                }

                if (_formState[s_formStateMdiChildMax] != 0)
                {
                    cp.Style |= (int)WINDOW_STYLE.WS_MAXIMIZE;
                }

                cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_MDICHILD;
            }

            if (TopLevel || IsMdiChild)
            {
                FillInCreateParamsStartPosition(cp);
                // Delay setting to visible until after the handle gets created
                // to allow applyClientSize to adjust the size before displaying
                // the form.
                //
                if ((cp.Style & (int)WINDOW_STYLE.WS_VISIBLE) != 0)
                {
                    _formState[s_formStateShowWindowOnCreate] = 1;
                    cp.Style &= ~(int)WINDOW_STYLE.WS_VISIBLE;
                }
                else
                {
                    _formState[s_formStateShowWindowOnCreate] = 0;
                }
            }

            if (RightToLeft == RightToLeft.Yes && RightToLeftLayout)
            {
                // We want to turn on mirroring for Form explicitly.
                cp.ExStyle |= (int)(WINDOW_EX_STYLE.WS_EX_LAYOUTRTL | WINDOW_EX_STYLE.WS_EX_NOINHERITLAYOUT);
                // Don't need these styles when mirroring is turned on.
                cp.ExStyle &= ~(int)(WINDOW_EX_STYLE.WS_EX_RTLREADING | WINDOW_EX_STYLE.WS_EX_RIGHT | WINDOW_EX_STYLE.WS_EX_LEFTSCROLLBAR);
            }

            return cp;
        }
    }

从这个源码可以知道,我们设置Form的FormBorderStyle/WindowState/TopLevel /IsMdiChild/RightToLeft 等属性后,CreateParams都会改变。
Form的继承关系如下:Control->ScrollableControl->ContainerControl->Form.
CreateParams就定义在Control类中,是一个虚方法,子类可以重写。
继续寻找源码,看到这里使用了CreateParams:
https://github.com/dotnet/winforms/blob/656dc8153a0bc464063ef9a4112f2eb94dcd100f/src/System.Windows.Forms/src/System/Windows/Forms/Form.cs#L1634:

/// <summary>
    ///  Determines the opacity of the form. This can only be set on top level controls.
    ///  Opacity requires Windows 2000 or later, and is ignored on earlier operating systems.
    /// </summary>
    [SRCategory(nameof(SR.CatWindowStyle))]
    [TypeConverter(typeof(OpacityConverter))]
    [SRDescription(nameof(SR.FormOpacityDescr))]
    [DefaultValue(1.0)]
    public double Opacity
    {
        get => Properties.GetValueOrDefault(s_propOpacity, 1.0d);
        set
        {
            value = Math.Clamp(value, 0.0d, 1.0d);

            Properties.AddOrRemoveValue(s_propOpacity, value, defaultValue: 1.0d);

            bool oldLayered = _formState[s_formStateLayered] != 0;

            if (OpacityAsByte < 255)
            {
                AllowTransparency = true;
                if (_formState[s_formStateLayered] != 1)
                {
                    _formState[s_formStateLayered] = 1;
                    if (!oldLayered)
                    {
                        UpdateStyles();
                    }
                }
            }
            else
            {
                _formState[s_formStateLayered] = (TransparencyKey != Color.Empty) ? 1 : 0;
                if (oldLayered != (_formState[s_formStateLayered] != 0))
                {
                    CreateParams cp = CreateParams;
                    if ((int)ExtendedWindowStyle != cp.ExStyle)
                    {
                        PInvokeCore.SetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, cp.ExStyle);
                    }
                }
            }

            UpdateLayered();
        }
    }

最后使用了PInvokeCore.SetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, cp.ExStyle);
cp就是我们的CreateParams参数。user32.dll中也有SetWindowLong函数,于是我们做一下尝试,在wpf中也像这样调用:

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    const int WS_EX_TOOLWINDOW = 0x00000080;
    const int WS_EX_NOACTIVATE = 0x08000000;
    const int GWL_EXSTYLE = -20;
    public MainWindow()
    {
        InitializeComponent();
        this.Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        WindowInteropHelper helper = new WindowInteropHelper(this);

        int GWL_EXSTYLE = -20;

        var style = User32Helper.GetWindowLong(helper.Handle, GWL_EXSTYLE);

        //cp.ExStyle |= (WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW);
        //cp.Parent = IntPtr.Zero; // Keep this line only if you used UserControl

        style |= (WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW);

        User32Helper.SetWindowLong(helper.Handle, GWL_EXSTYLE, new IntPtr(style));
    }
}

最后发现OK啦。

向其他具有焦点的程序发送文本
  • 获取具有焦点的活动窗口

     /// <summary>
     /// 获取当前活动窗口
     /// </summary>
     /// <returns></returns>
     [DllImport("user32.dll")]
     public static extern IntPtr GetForegroundWindow();
    
  • 向指定句柄的窗口发送文本:
    查了好多资料,都说调用user32.dll的SendMessage函数,但是发现没有达到想要的效果。
    最后发现使用winform包下的SendKeys.SendWait函数可以满足需求,暂时没有深挖SendKeys.SendWait背后到底是调用了哪些user32.dll函数。
    将.net8的wpf程序引用winform包:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net8.0-windows</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <UseWPF>true</UseWPF>
    	<UseWindowsForms>true</UseWindowsForms>
      </PropertyGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\Common\Common.csproj" />
      </ItemGroup>
    
    </Project>
    

    需要添加<UseWindowsForms>true</UseWindowsForms>
    代码如下:

    try
    {
        var handle = User32Helper.GetForegroundWindow();
        string text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
    
        #region 不起作用
        //User32Helper.COPYDATASTRUCT cds;
        //cds.dwData= (IntPtr)0;
        //cds.cbData = text.Length + 1;
        //cds.lpData = text;
    
        //var result= User32Helper.SendMessage(handle, WM_SETTEXT, IntPtr.Zero, ref cds);
    
    
        //Marshal.FreeHGlobal(buffer);
        #endregion
    
    
        SendKeys.SendWait(text);
    }
    catch (Exception ee)
    {
        System.Windows.MessageBox.Show(ee.Message);
    }
    

    最后实现的效果如下:
    在这里插入图片描述
    可以看到中间发送的时间好像有乱码一样,是因为我把输入法换成了中文了,所以成这样了。
    我尝试过,发送中文也是没有任何问题的。解决了以上问题,离语音转文字输入法的想法又更近一步啦。

WPF实现悬浮球效果

要求如下:
1.窗口置于最顶端
2.无边框圆形窗体
3.能够拖动
4.能够响应鼠标点击
5.不抢占焦点

经过几天的折腾,最后实现如下:

MainWindow.xaml

<Window
    x:Class="Test.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:converters="clr-namespace:YOFC.SpeechInput.Converters"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:local="clr-namespace:Test"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="100"
    Height="100"
    d:DataContext="{d:DesignInstance local:MainWindowViewModel}"
    AllowsTransparency="True"
    Background="Transparent"
    ResizeMode="NoResize"
    ShowInTaskbar="False"
    Topmost="True"
    WindowStyle="None"
    mc:Ignorable="d">
    <Window.Resources>
        <Style x:Key="FocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Rectangle
                            Margin="2"
                            SnapsToDevicePixels="true"
                            Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"
                            StrokeDashArray="1 2"
                            StrokeThickness="1" />
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <SolidColorBrush x:Key="Button.Static.Background" Color="#FFDDDDDD" />
        <SolidColorBrush x:Key="Button.Static.Border" Color="#FF707070" />
        <SolidColorBrush x:Key="Button.MouseOver.Background" Color="#FFBEE6FD" />
        <SolidColorBrush x:Key="Button.MouseOver.Border" Color="#FF3C7FB1" />
        <SolidColorBrush x:Key="Button.Pressed.Background" Color="#FFC4E5F6" />
        <SolidColorBrush x:Key="Button.Pressed.Border" Color="#FF2C628B" />
        <SolidColorBrush x:Key="Button.Disabled.Background" Color="#FFF4F4F4" />
        <SolidColorBrush x:Key="Button.Disabled.Border" Color="#FFADB2B5" />
        <SolidColorBrush x:Key="Button.Disabled.Foreground" Color="#FF838383" />
        <Style x:Key="CircleButton" TargetType="{x:Type Button}">
            <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}" />
            <Setter Property="Background" Value="{StaticResource Button.Static.Background}" />
            <Setter Property="BorderBrush" Value="{StaticResource Button.Static.Border}" />
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
            <Setter Property="BorderThickness" Value="1" />
            <Setter Property="HorizontalContentAlignment" Value="Center" />
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="Padding" Value="1" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Grid>
                            <Ellipse Fill="{TemplateBinding Background}" />
                            <ContentPresenter
                                x:Name="contentPresenter"
                                Margin="{TemplateBinding Padding}"
                                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                Focusable="False"
                                RecognizesAccessKey="True"
                                SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <SolidColorBrush x:Key="FlashColor">LightBlue</SolidColorBrush>

        <converters:BooleanToInVisibilityConverter x:Key="BooleanToInVisibilityConverter" />
        <converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
    </Window.Resources>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding LoadedCmd}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid Background="Transparent">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="PreviewMouseLeftButtonDown">
                <i:InvokeCommandAction Command="{Binding MouseLeftButtonDownCmd}" PassEventArgsToCommand="True" />
            </i:EventTrigger>
            <i:EventTrigger EventName="PreviewMouseLeftButtonUp">
                <i:InvokeCommandAction Command="{Binding MouseLeftButtonUpCmd}" PassEventArgsToCommand="True" />
            </i:EventTrigger>
            <i:EventTrigger EventName="PreviewMouseMove">
                <i:InvokeCommandAction Command="{Binding MouseMoveCmd}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
        <Grid Visibility="{Binding IsRecording, Converter={StaticResource BooleanToVisibilityConverter}}">
            <Grid.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <Storyboard
                            AutoReverse="True"
                            FillBehavior="HoldEnd"
                            RepeatBehavior="Forever"
                            Duration="0:0:0.5">
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetName="ellipse1_0"
                                    Storyboard.TargetProperty="Height"
                                    From="60"
                                    To="100" />
                                <DoubleAnimation
                                    Storyboard.TargetName="ellipse1_0"
                                    Storyboard.TargetProperty="Width"
                                    From="60"
                                    To="100" />
                            </Storyboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetName="ellipse0_8"
                                    Storyboard.TargetProperty="Height"
                                    From="80"
                                    To="100" />
                                <DoubleAnimation
                                    Storyboard.TargetName="ellipse0_8"
                                    Storyboard.TargetProperty="Width"
                                    From="80"
                                    To="100" />
                            </Storyboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetName="ellipse0_4"
                                    Storyboard.TargetProperty="Height"
                                    From="40"
                                    To="100" />
                                <DoubleAnimation
                                    Storyboard.TargetName="ellipse0_4"
                                    Storyboard.TargetProperty="Width"
                                    From="40"
                                    To="100" />
                            </Storyboard>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Grid.Triggers>
            <Ellipse
                Name="ellipse1_0"
                Fill="{StaticResource FlashColor}"
                Opacity="1" />
            <Ellipse
                Name="ellipse0_8"
                Fill="{StaticResource FlashColor}"
                Opacity="0.8" />
            <Ellipse
                Name="ellipse0_4"
                Fill="{StaticResource FlashColor}"
                Opacity="0.4" />
        </Grid>
        <Button
            Width="100"
            Height="100"
            Padding="0"
            Background="LightBlue"
            Command="{Binding StartOrStopRecordCmd}"
            Opacity="0.8"
            Style="{DynamicResource CircleButton}">
            <Grid Visibility="{Binding IsConnected, Converter={StaticResource BooleanToVisibilityConverter}}">
                <Viewbox Width="50" Height="50">
                    <Path
                        Data="M661.9136 329.1136c12.1856 0 18.3296 6.144 18.3296 18.3296l0 182.8864c0 62.5664-20.992 116.224-62.8736 161.1776C575.488 736.3584 523.264 761.9584 460.8 768l0 219.4432 91.4432 0c12.1856 0 18.3296 6.144 18.3296 18.3296 0 12.288-6.144 18.3296-18.3296 18.3296L332.8 1024.1024c-12.1856 0-18.3296-6.0416-18.3296-18.3296 0-12.1856 6.144-18.3296 18.3296-18.3296l91.4432 0L424.2432 768c-62.464-6.0416-114.688-31.6416-156.5696-76.5952C225.792 646.5536 204.8 592.7936 204.8 530.3296L204.8 347.4432c0-12.1856 6.144-18.3296 18.3296-18.3296s18.3296 6.144 18.3296 18.3296l0 182.8864c0 56.4224 19.456 104.0384 58.2656 142.848 38.8096 38.8096 86.4256 58.2656 142.848 58.2656 56.4224 0 104.0384-19.456 142.848-58.2656 38.912-38.8096 58.2656-86.4256 58.2656-142.848L643.6864 347.4432C643.6864 335.2576 649.728 329.1136 661.9136 329.1136zM325.9392 646.8608c31.9488 31.9488 70.8608 48.0256 116.5312 48.0256 45.6704 0 84.5824-15.9744 116.5312-48.0256s48.0256-70.8608 48.0256-116.5312L607.0272 164.5568c0-45.6704-15.9744-84.5824-48.0256-116.5312C527.0528 15.9744 488.2432 0 442.4704 0 396.8 0 357.9904 15.9744 325.9392 48.0256 293.9904 79.9744 277.9136 118.8864 277.9136 164.5568l0 365.6704C277.9136 576 293.9904 614.8096 325.9392 646.8608zM352.256 74.24C377.344 49.152 407.4496 36.5568 442.4704 36.5568S507.6992 49.152 532.7872 74.24c25.1904 25.1904 37.6832 55.1936 37.6832 90.3168l0 365.6704c0 35.1232-12.5952 65.1264-37.6832 90.3168C507.6992 645.7344 477.5936 658.3296 442.4704 658.3296S377.344 645.7344 352.256 620.544C327.0656 595.456 314.4704 565.3504 314.4704 530.3296L314.4704 164.5568C314.4704 129.536 327.0656 99.4304 352.256 74.24z"
                        Fill="#1296db"
                        Stretch="Uniform" />
                </Viewbox>
            </Grid>
        </Button>
        <Grid Visibility="{Binding IsConnected, Converter={StaticResource BooleanToInVisibilityConverter}}">
            <Ellipse Fill="LightBlue" />
            <Viewbox
                Width="50"
                Height="50"
                Visibility="Visible">
                <Path
                    Data="M316.8 557.6a15.632 15.632 0 0 0-9.6 4.208c-2.72 2.864-3.328 7.184-3.184 9.456 6.576 102.16 86.352 184.32 187.184 193.952v53.216a16 16 0 0 0 16 16h16a16 16 0 0 0 16-16v-52.944c20.72-1.664 40.864-6.4 59.936-14.016a12.912 12.912 0 0 0 6.624-6.944c1.344-3.472 1.072-8.08 0.176-9.952l-9.952-20.864c-0.848-1.76-3.392-5.264-7.2-6.464-3.472-1.12-8.16-0.064-10.112 0.688a158.144 158.144 0 0 1-56.144 10.224h-11.456c-84 0-152.88-65.392-159.04-148.432a16.96 16.96 0 0 0-3.872-8.4 12.128 12.128 0 0 0-8.224-3.744z m40.64-350.56a16 16 0 0 0-22.24-4.192l-13.216 9.024a16 16 0 0 0-4.192 22.24l366.88 536.928a16 16 0 0 0 22.24 4.192l13.216-9.024a16 16 0 0 0 4.176-22.24z m47.36 193.216a168.64 168.64 0 0 0-21.6 0c-10.4 1.072-13.6 8.176-13.6 12.272v123.84a147.872 147.872 0 0 0 192.688 140.96c4.112-0.976 8.112-7.2 4.56-15.2l-8.256-21.008c-1.792-7.792-9.792-10.672-14.96-8.32a99.872 99.872 0 0 1-126.032-96.432v-123.84c0-8-5.712-11.2-12.8-12.272z m289.344 157.328c-2.256 0.016-6.032 0.208-8.544 2.144-3.088 2.368-4.416 7.184-4.832 9.536-2.032 11.408-6.4 26.752-12.272 43.36-0.688 1.984-1.888 6.896-0.496 10.304 1.424 3.44 5.456 5.376 7.36 6.08l21.456 8.176c2.08 0.8 7.552 2.784 11.344 0.944 2.976-1.456 4.544-6.48 5.152-8.192l0.416-1.2c8.08-22.8 13.76-43.584 15.632-60.192a10.4 10.4 0 0 0-3.76-8.176c-2.56-2.08-6.72-2.784-8.8-2.784zM517.472 192a147.36 147.36 0 0 0-90.88 31.2c-7.392 4.848-8.992 14.928-1.984 21.856l12.112 12.368c6.48 6.304 16.08 4.704 22.656 1.2a99.872 99.872 0 0 1 157.984 81.264v196.48c0 5.44 1.84 13.76 12.784 15.328l20 2.688c9.056 0.544 14.864-6.016 15.056-11.2l0.16-4.48V339.856A147.872 147.872 0 0 0 517.472 192z"
                    Fill="Red"
                    Stretch="Uniform" />
            </Viewbox>
        </Grid>
    </Grid>
</Window>

MainWindowViewModel.cs

public partial class MainWindowViewModel : ObservableObject
{
    const int WS_EX_TOOLWINDOW = 0x00000080;
    const int WS_EX_NOACTIVATE = 0x08000000;
    const int GWL_EXSTYLE = -20;

    private DateTime _lastPressedTime;
    private bool _isPressed = false;
    private POINT _lastPoint;
    public MainWindowViewModel()
    {
        StartOrStopRecordCmd = new RelayCommand(StartOrStopRecord, () => this.IsConnected);
        this.LoadedCmd = new RelayCommand(Loaded);
        this.MouseLeftButtonDownCmd = new RelayCommand<MouseButtonEventArgs>(MouseLeftButtonDown);
        this.MouseLeftButtonUpCmd = new RelayCommand<MouseButtonEventArgs>(MouseLeftButtonUp);
        this.MouseMoveCmd = new RelayCommand(MouseMove);
    }

    private void MouseMove()
    {
        if (_isPressed)
        {
            User32Helper.GetCursorPos(out var point);

            App.Current.MainWindow.Left = App.Current.MainWindow.Left + point.X - _lastPoint.X;
            App.Current.MainWindow.Top = App.Current.MainWindow.Top + point.Y - _lastPoint.Y;

            //赋值
            _lastPoint = point;
        }
    }

    private void MouseLeftButtonUp(MouseButtonEventArgs? e)
    {
        _isPressed = false;
        //长按的话,不响应Button点击事件
        if ((DateTime.Now - _lastPressedTime).TotalSeconds > 0.5)
        {
            e.Handled = true;
        }
    }

    private void MouseLeftButtonDown(MouseButtonEventArgs? e)
    {
        _isPressed = true;
        _lastPressedTime = DateTime.Now;

        User32Helper.GetCursorPos(out _lastPoint);
    }

    private void Loaded()
    {
        var helper = new WindowInteropHelper(App.Current.MainWindow);

        int GWL_EXSTYLE = -20;

        var style = User32Helper.GetWindowLong(helper.Handle, GWL_EXSTYLE);

        style |= (WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW);

        User32Helper.SetWindowLong(helper.Handle, GWL_EXSTYLE, new IntPtr(style));

        App.Current.MainWindow.Left = SystemParameters.PrimaryScreenWidth - App.Current.MainWindow.Width;
        App.Current.MainWindow.Top = SystemParameters.PrimaryScreenHeight / 2;
    }

    private async void StartOrStopRecord()
    {
        if (!IsConnected)
        {
            MessageBox.Show("未连接到服务器", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
            return;
        }
        if (IsRecording)
        {
            await speechInput.Stop();
            IsRecording = false;
        }
        else
        {
            await speechInput.Start();
            IsRecording = true;
        }
    }

    ~MainWindowViewModel()
    {
        speechInput.Unload();
    }
    /// <summary>
    /// 是否正在录音
    /// </summary>
    [ObservableProperty] private bool _isRecording;
    /// <summary>
    /// 是否连接上了服务端
    /// </summary>
    [NotifyCanExecuteChangedFor(nameof(StartOrStopRecordCmd))]
    [ObservableProperty] private bool _isConnected;

    public RelayCommand StartOrStopRecordCmd { get; }
    public RelayCommand<MouseButtonEventArgs> MouseLeftButtonDownCmd { get; }
    public RelayCommand<MouseButtonEventArgs> MouseLeftButtonUpCmd { get; }
    public RelayCommand MouseMoveCmd { get; }
    public RelayCommand LoadedCmd { get; }
}

最后实现效果:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/892598.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Py之pygetwindow:pygetwindow的简介、安装和使用方法、案例应用之详细攻略

Py之pygetwindow&#xff1a;pygetwindow的简介、安装和使用方法、案例应用之详细攻略 目录 pygetwindow的简介 pygetwindow的安装和使用方法 pygetwindow的案例应用 1、使用了Windows系统打开了记事本应用程序&#xff0c;其窗口标题为“无标题 - 记事本” 2、Window对象…

STM32学习笔记---RTC

目录 一、什么是RTC 二、如何配置RTC 1、标准实时时钟部分(万年历部分) 1.1 时钟源分类 1.2 RTC时钟源的选择 1.3 精密校正 1.4 异步7位预分频器 1.5 粗略校正 1.6 同步15位分频 1.7 日历寄存器 1.8 RTC的初始化与配置 1.9 程序设计 2、闹钟部分 2.1 闹钟的初始化…

Python酷库之旅-第三方库Pandas(155)

目录 一、用法精讲 706、pandas.DatetimeTZDtype类 706-1、语法 706-2、参数 706-3、功能 706-4、返回值 706-5、说明 706-6、用法 706-6-1、数据准备 706-6-2、代码示例 706-6-3、结果输出 707、pandas.Timedelta.asm8属性 707-1、语法 707-2、参数 707-3、功能…

信息学CCF CSP-J/S 2024常见问题汇总,低年级考生重点关注

摘要 随着2024年CSP-J/S初赛的临近&#xff0c;各省报名要求细则陆续公布。为了帮助广大考生和家长准确了解各省政策&#xff0c;自主选拔在线团队特为汇总整理全国各省CSP-J/S2024认证相关问题&#xff0c;希望可以帮助各位考生更好的备考&#xff01; CCF CSP-J/S 2024 认证…

Android平台RTSP|RTMP播放器PK:VLC for Android还是SmartPlayer?

好多开发者&#xff0c;希望在Android端低延迟的播放RTMP或RTSP流&#xff0c;本文就目前市面上主流2个直播播放框架&#xff0c;做个简单的对比。 VLC for Android VLC for Android 是一款功能强大的多媒体播放器&#xff0c;具有以下特点和功能&#xff1a; 广泛的格式支持…

PDF-XChange PRO v10.4.2.390 x64 已授权中文特别版

PDF-XChange PRO是一款功能强大的PDF编辑和查看软件&#xff0c;PDF-XChange PRO 一个多合一的PDF解决方案。这是Tracker Software的三个最佳应用程序的套件&#xff1a;PDF-XChange Editor Plus&#xff0c;PDF-Tools和PDF-XChange Standard。使用 PDF-XChange Editor Plus&am…

vector的深入剖析与底层逻辑

前言&#xff1a; 上篇我们谈到vector的概念&#xff0c;使用&#xff0c;以及相关接口的具体应用&#xff0c;本文将对vector进行深入剖析&#xff0c;为读者分享其底层逻辑&#xff0c;讲解其核心细节。 上篇链接&#xff1a; 初始vector——数组的高级产物-CSDN博客 一.…

CDGA|数据治理:如何让传统行业实现数据智能

在当今这个数字化时代&#xff0c;数据已成为推动各行各业转型升级的关键力量。对于传统行业而言&#xff0c;如何从海量、复杂的数据中挖掘价值&#xff0c;实现“数据智能”&#xff0c;成为了提升竞争力、优化运营效率、创新业务模式的重要途径。本文将探讨数据治理如何助力…

【文献及模型、制图分享】干旱区山水林田湖草沙冰一体化保护与系统治理——基于土地退化平衡视角

文献介绍 目标明晰、统筹兼顾、干预适度是山水林田湖草沙冰一体化保护与系统治理的客观要求。基于土地退化平衡&#xff08;LDN&#xff09;视角&#xff0c;构建涵盖双重对象、双重法则、双重原则、指标体系、价值取向的理论框架&#xff0c;并以天山北坡城市群为例&#xff…

Flume抽取数据(包含自定义拦截器和时间戳拦截器)

flume参考网址&#xff1a;Flume 1.9用户手册中文版 — 可能是目前翻译最完整的版本了https://flume.liyifeng.org/?flagfromDoc#要求&#xff1a; 使用Flume将日志抽取到hdfs上&#xff1a;通过java代码编写一个拦截器&#xff0c;将日志中不是json数据的数据过滤掉&#xf…

模拟退火算法最常见知识点详解与原理简介控制策略

章节目录 模拟退火算法简介与原理 算法的基本流程与步骤 关键参数与控制策略 模拟退火算法的应用领域 如何学习模拟退火算法 资源简介与总结 一、模拟退火算法简介与原理 重点详细内容知识点总结 1. 模拟退火算法简介 模拟退火算法&#xff08;Simulated Annealing, SA&#x…

blender分离含有多个动作的模型,并导出含有材质的fbx模型

问题背景 笔者是模型小白&#xff0c;需要将网络上下载的fbx模型中的动作&#xff0c;分离成单独的动作模型&#xff0c;经过3天摸爬滚打&#xff0c;先后使用了blender&#xff0c;3d max&#xff0c;unity&#xff0c;最终用blender完成&#xff0c;期间参考了众多网络上大佬…

Spring Boot框架下大创项目流程自动化

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理大创管理系统的相关信息成为必然。开发合适…

DETR[端到端目标检测](论文复现)

DETR[端到端目标检测](论文复现) 本文所涉及所有资源均在传知代码平台可获取 文章目录 DETR[端到端目标检测](论文复现)概述模型主体框架演示效果核心逻辑使用方式部署方式数据准备概述 在目标检测需要许多手工设计的组件,例如非极大值抑制(NMS),基于人工经验生成的先验…

【Trulens框架】用TruLens 自动化 RAG 应用项目评估测试

前言&#xff1a; 什么是Trulens TruLens是面向神经网络应用的质量评估工具&#xff0c;它可以帮助你使用反馈函数来客观地评估你的基于LLM&#xff08;语言模型&#xff09;的应用的质量和效果。反馈函数可以帮助你以编程的方式评估输入、输出和中间结果的质量&#xff0c;从而…

Gin框架操作指南10:服务器与高级功能

官方文档地址&#xff08;中文&#xff09;&#xff1a;https://gin-gonic.com/zh-cn/docs/ 注&#xff1a;本教程采用工作区机制&#xff0c;所以一个项目下载了Gin框架&#xff0c;其余项目就无需重复下载&#xff0c;想了解的读者可阅读第一节&#xff1a;Gin操作指南&#…

SICK系列激光雷达单点测距仪DT80-311111+SIG200配置和通信

文章目录 一、硬件连接与SOPAS连接测距仪二、从SOPAS读取数据三、通过JSON获取数据1. 使用Postman测试接口2. 通过代码实现 一、硬件连接与SOPAS连接测距仪 首先硬件设备连接如下&#xff1a; 电源厂家应该是不提供&#xff0c;需要自行解决。 安装完成后需要使用sick的SOPAS…

增量知识 (Incremental Knowledge, IK)

在语义通信系统中&#xff0c;增量知识&#xff08;IK, Incremental Knowledge&#xff09;是一种增强数据传输效率和可靠性的技术&#xff0c;特别是用于混合自动重传请求&#xff08;HARQ, Hybrid Automatic Repeat reQuest&#xff09;机制时。它的核心思想是在传输失败后&a…

图像中的融合

图像显示函数 def img_show(name, img):"""显示图片:param name: 窗口名字:param img: 图片对象:return: None"""cv2.imshow(name, img)cv2.waitKey(0)cv2.destroyAllWindows()图像读取与处理 读取图片 cloud cv2.imread(bg.jpg) fish cv2.…

【uni-app】HBuilderX安装uni-ui组件

目录 1、官网找到入口 2、登录帐号 3、打开HuilderX 4、选择要应用的项目 5、查看是否安装完成 6、按需安装 7、安装完毕要重启 8、应用 前言&#xff1a;uniapp项目使用uni-ui组件方式很多&#xff0c;有npm安装等&#xff0c;或直接创建uni-ui项目&#xff0c;使用un…