WPF 使用快捷鍵方式製作簡易的 Word 上的 Latex 輸入法

lindexi發表於2024-10-24

軟體的介面效果如下:

執行效果如下:

本文以下為演示應用,沒有包含從使用者輸入生成 Latex 格式的公式,僅僅只包含核心的如何在 Word 插入公式部分

本文的核心實現邏輯是根據 WPF 拼音輸入法 實現的,只是有所不同的是沒有進行鍵盤鉤子,而是代替為手動點選按鈕

點選傳送按鈕將 Latex 公式輸入到 Word 裡作為 Word 公式,其核心的方法就是使用 Word 的快捷鍵插入公式編輯器,再透過剪貼簿輸入 Latex 格式的公式內容,再輸入回車作為 Word 公式。按鈕的點選的實現程式碼如下

    private void SendButton_OnClick(object sender, RoutedEventArgs e)
    {
        SendKeys.SendWait("%="); // 傳送 alt+= 讓Word開啟公式編輯器
        Clipboard.SetText("a^2+b^2=c^2"); // 將文字放入剪貼簿
        SendKeys.SendWait("^v"); // 傳送 ctrl+v 貼上文字
        SendKeys.SendWait("{Enter}"); // 傳送Enter鍵讓 Latex 公式成為 Word 公式
    }

上文這裡固定傳送的是 a^2+b^2=c^2 公式,大家可以根據需求自行替換為其他公式

以下為整個專案的詳細實現方法

先建立一個 .NET 9 的 WPF 專案,建立之後記得勾選 WinForms 引用,如不知道在哪勾選,可以雙擊專案檔案,將 csproj 專案檔案替換為如下程式碼

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net9.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWindowsForms>True</UseWindowsForms>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

以上程式碼裡面的 <UseWindowsForms>True</UseWindowsForms> 就是用來新增對 WinForms 的引用。如果大家看了以上配置還是不知道如何做,可以在本文末尾找到本文所有程式碼的下載方法

接著開啟 MainWindow.xaml 檔案,寫一個固定且簡單的輸入法介面

    <Grid>
        <TextBlock HorizontalAlignment="Left" VerticalAlignment="Top"
                   FontSize="20">
            <Run Foreground="Gray" Text="1."/>
            <Run Foreground="Black" Text="a^2+b^2=c^2"></Run>
        </TextBlock>
        <Button x:Name="SendButton"
                Margin="10 10 10 10"
                HorizontalAlignment="Right" VerticalAlignment="Bottom"
                Click="SendButton_OnClick">傳送</Button>
    </Grid>

以上介面固定了一個輸入法候選公式,和一個按鈕

繼續編輯 MainWindow.xaml 檔案,設定一些視窗屬性

        WindowStyle="ToolWindow"
        Topmost="True"
        Title="Latex Word 輸入法" Height="100" Width="300"

按鈕點選的 SendButton_OnClick 方法的實現已經在上文告訴大家

作為一個輸入法,不應該讓視窗獲取焦點,否則將會搶走應用的焦點。按照 .NET/C# 使視窗永不啟用(No Activate 永不獲得焦點) - walterlv 部落格提供的方法,在 MainWindow.xaml.cs 配置讓視窗不獲取焦點

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);
        var windowInteropHelper = new WindowInteropHelper(this);
        Win32WindowHelper.SetNoActivate(windowInteropHelper.Handle);
    }

public static class Win32WindowHelper
{
    /// <summary>
    /// 使視窗永不啟用
    /// </summary>
    /// <param name="hWnd"></param>
    /// [.NET/C# 使視窗永不啟用(No Activate 永不獲得焦點) - walterlv](https://blog.walterlv.com/post/no-activate-window.html )
    public static void SetNoActivate(IntPtr hWnd)
    {
        var exStyle = GetWindowLong(hWnd, GWL_EXSTYLE);
        SetWindowLong(hWnd, GWL_EXSTYLE, new IntPtr(exStyle.ToInt32() | WS_EX_NOACTIVATE));
    }

    private const int WS_EX_NOACTIVATE = 0x08000000;
    private const int GWL_EXSTYLE = -20;

    public static IntPtr GetWindowLong(IntPtr hWnd, int nIndex)
    {
        return Environment.Is64BitProcess
            ? GetWindowLong64(hWnd, nIndex)
            : GetWindowLong32(hWnd, nIndex);
    }

    public static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
    {
        return Environment.Is64BitProcess
            ? SetWindowLong64(hWnd, nIndex, dwNewLong)
            : SetWindowLong32(hWnd, nIndex, dwNewLong);
    }

    [DllImport("user32.dll", EntryPoint = "GetWindowLong")]
    private static extern IntPtr GetWindowLong32(IntPtr hWnd, int nIndex);

    [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
    private static extern IntPtr GetWindowLong64(IntPtr hWnd, int nIndex);

    [DllImport("user32.dll", EntryPoint = "SetWindowLong")]
    private static extern IntPtr SetWindowLong32(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

    [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
    private static extern IntPtr SetWindowLong64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
}

整個 MainWindow.xaml.cs 的核心程式碼如下

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);
        var windowInteropHelper = new WindowInteropHelper(this);
        Win32WindowHelper.SetNoActivate(windowInteropHelper.Handle);
    }

    private void SendButton_OnClick(object sender, RoutedEventArgs e)
    {
        SendKeys.SendWait("%="); // 傳送 alt+= 讓Word開啟公式編輯器
        Clipboard.SetText("a^2+b^2=c^2"); // 將文字放入剪貼簿
        SendKeys.SendWait("^v"); // 傳送 ctrl+v 貼上文字
        SendKeys.SendWait("{Enter}"); // 傳送Enter鍵讓 Latex 公式成為 Word 公式
    }
}

public static class Win32WindowHelper
{
    /// <summary>
    /// 使視窗永不啟用
    /// </summary>
    /// <param name="hWnd"></param>
    /// [.NET/C# 使視窗永不啟用(No Activate 永不獲得焦點) - walterlv](https://blog.walterlv.com/post/no-activate-window.html )
    public static void SetNoActivate(IntPtr hWnd)
    {
        var exStyle = GetWindowLong(hWnd, GWL_EXSTYLE);
        SetWindowLong(hWnd, GWL_EXSTYLE, new IntPtr(exStyle.ToInt32() | WS_EX_NOACTIVATE));
    }

    private const int WS_EX_NOACTIVATE = 0x08000000;
    private const int GWL_EXSTYLE = -20;

    public static IntPtr GetWindowLong(IntPtr hWnd, int nIndex)
    {
        return Environment.Is64BitProcess
            ? GetWindowLong64(hWnd, nIndex)
            : GetWindowLong32(hWnd, nIndex);
    }

    public static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
    {
        return Environment.Is64BitProcess
            ? SetWindowLong64(hWnd, nIndex, dwNewLong)
            : SetWindowLong32(hWnd, nIndex, dwNewLong);
    }

    [DllImport("user32.dll", EntryPoint = "GetWindowLong")]
    private static extern IntPtr GetWindowLong32(IntPtr hWnd, int nIndex);

    [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
    private static extern IntPtr GetWindowLong64(IntPtr hWnd, int nIndex);

    [DllImport("user32.dll", EntryPoint = "SetWindowLong")]
    private static extern IntPtr SetWindowLong32(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

    [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
    private static extern IntPtr SetWindowLong64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
}

本文程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 18e21d7acfd12a55b04f554cbe5ce770e37518ef

以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼。如果依然拉取不到程式碼,可以發郵件向我要程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 18e21d7acfd12a55b04f554cbe5ce770e37518ef

獲取程式碼之後,進入 WPFDemo/QarchananaFeweajeka 資料夾,即可獲取到原始碼

開啟 QarchananaFeweajeka.sln 檔案,理論上就可以按下 F5 讓 VisualStudio 構建且執行。試試新建一個 Word 文件,進入輸入狀態,然後點選執行起來的輸入法程式的傳送按鈕。預計就可以看到在 Word 輸入了一段公式

更多技術部落格,請參閱 部落格導航

相關文章