头图

前言

为什么这么做?

因为我需要在 C# 代码中执行一些用 python 写的算法,所以自然有这样的需求。

是什么项目?

本示例是使用 WPF 项目

xaml

我们首先编写前端文件,定义出所用的控件

<Window x:Class="Wpf_py_demo.MainWindow"
        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:local="clr-namespace:Wpf_py_demo"
        mc:Ignorable="d"
        Title="MainWindow" d:DesignHeight="400" d:DesignWidth="600">
    <Grid ShowGridLines="False">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- 中间区域,使用 Grid 三行三列布局 -->
        <Grid Grid.Row="1">
            <!-- 三行:文件行、目录行、进度行 -->
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <!-- 三列:标签(Auto)、输入区(*)、按钮(Auto) -->
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>

            <!-- 第一行:截面带数据文件 -->
            <TextBlock 
                Grid.Row="0" Grid.Column="0"
                Text="截面带数据文件:"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                FontWeight="Bold"
                Margin="10,5,10,0"/>
            <TextBox 
                Grid.Row="0" Grid.Column="1"
                x:Name="FilePath" Height="50"
                IsReadOnly="True"
                TextWrapping="Wrap"
                Text="C:\AAA-personalData\project\00-graduateProject\001-Laser_Profile\WPF_PCV\pyFunc\Comparison\Data\250606\20250606103010____ProcsData.txt"
                VerticalAlignment="Center"
                VerticalContentAlignment="Center"
                Margin="0,10,0,5"/>
            <Button 
                Grid.Row="0" Grid.Column="2"
                x:Name="ChooseFile"
                Click="ChooseFile_Click"
                Content="选择文件" Height="30"
                Margin="10,5,10,0"/>

            <!-- 第二行:CMM 目录 -->
            <TextBlock 
                Grid.Row="1" Grid.Column="0"
                Text="CMM 目录:"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                FontWeight="Bold"
                Margin="10,5"/>
            <TextBox 
                Grid.Row="1" Grid.Column="1"
                x:Name="FolderPath" Height="50"
                IsReadOnly="True"
                TextWrapping="Wrap"
                Text="C:\AAA-personalData\project\00-graduateProject\001-Laser_Profile\WPF_PCV\pyFunc\Comparison\Data\cmm0609"
                VerticalAlignment="Center"
                VerticalContentAlignment="Center"
                Margin="0,5,0,5"/>
            <Button 
                Grid.Row="1" Grid.Column="2"
                x:Name="ChooseFolder"
                Click="ChooseFolder_Click"
                Content="选择CMM文件夹" Height="30"
                Margin="10,5"/>

            <!-- 第三行:进度 -->
            <TextBlock 
                Grid.Row="2" Grid.Column="0"
                Text="进度:"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                FontWeight="Bold"
                Margin="10,0,10,5"/>
            <ProgressBar 
                Grid.Row="2" Grid.Column="1"
                x:Name="PbReportProgress"
                Height="50"
                Minimum="0" Maximum="100" Value="0"
                VerticalAlignment="Center"
                Margin="0,5,0,10"/>
            <Button 
                Grid.Row="2" Grid.Column="2"
                x:Name="OutputPDF"
                Click="OutputPDF_Click"
                Content="一键生成PDF报告" Height="30"
                Margin="10,0,10,5"/>
        </Grid>
        <!-- 底部日志框 -->
        <TextBox
            Grid.Row="2"
            x:Name="TbProgressInfo"
            Height="165"
            Margin="10,5,10,0"
            Text="等待生成…"
            TextWrapping="Wrap"/>
    </Grid>
</Window>

CSharp

然后我们需要编写 code behind 逻辑

using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Win32;
using System.IO;
using System.Diagnostics;
using System.Threading.Tasks;
using System;

namespace Wpf_py_demo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 选择文件按钮点击事件
        /// </summary>
        private void ChooseFile_Click(object sender, RoutedEventArgs e)
        {
            Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog
            {
                Title = "选择截面带数据文件",
                Filter = "文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*",
                InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
            };

            if (openFileDialog.ShowDialog() == true)
            {
                FilePath.Text = openFileDialog.FileName;
            }
        }

        /// <summary>
        /// 选择文件夹按钮点击事件
        /// </summary>
        private void ChooseFolder_Click(object sender, RoutedEventArgs e)
        {
            using (var dialog = new System.Windows.Forms.FolderBrowserDialog())
            {
                dialog.Description = "选择CMM数据文件夹";
                dialog.ShowNewFolderButton = false;

                if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                {
                    FolderPath.Text = dialog.SelectedPath;
                }
            }
        }

        /// <summary>
        /// 生成PDF报告按钮点击事件
        /// </summary>
        private async void OutputPDF_Click(object sender, RoutedEventArgs e)
        {
            // 验证输入
            if (string.IsNullOrWhiteSpace(FilePath.Text))
            {
                System.Windows.MessageBox.Show("请先选择截面带数据文件!", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            if (string.IsNullOrWhiteSpace(FolderPath.Text))
            {
                System.Windows.MessageBox.Show("请先选择CMM数据文件夹!", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            if (!File.Exists(FilePath.Text))
            {
                System.Windows.MessageBox.Show("选择的文件不存在!", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            if (!Directory.Exists(FolderPath.Text))
            {
                System.Windows.MessageBox.Show("选择的文件夹不存在!", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            // 禁用按钮,防止重复点击
            OutputPDF.IsEnabled = false;
            PbReportProgress.Value = 0;
            TbProgressInfo.Text = "开始生成报告...\n";

            try
            {
                await RunPythonScript(FilePath.Text, FolderPath.Text);
            }
            catch (Exception ex)
            {
                System.Windows.MessageBox.Show($"生成报告时发生错误:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
                TbProgressInfo.Text += $"错误:{ex.Message}\n";
            }
            finally
            {
                // 重新启用按钮
                OutputPDF.IsEnabled = true;
                PbReportProgress.Value = 100;
            }
        }

        /// <summary>
        /// 修复编码问题
        /// </summary>
        private string FixEncoding(string input)
        {
            if (string.IsNullOrEmpty(input))
                return input;

            try
            {
                // 如果字符串看起来已经是正确的UTF-8,直接返回
                if (!input.Contains("�") && !HasEncodingIssues(input))
                {
                    return input;
                }

                // 尝试从Latin-1转换为UTF-8(常见的编码问题)
                byte[] bytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(input);
                string result = Encoding.UTF8.GetString(bytes);

                if (!result.Contains("�") && result.Length > 0)
                {
                    return result;
                }

                // 如果转换失败,返回原始字符串
                return input;
            }
            catch
            {
                // 如果转换失败,返回原始字符串
                return input;
            }
        }

        /// <summary>
        /// 检查字符串是否有编码问题
        /// </summary>
        private bool HasEncodingIssues(string input)
        {
            // 检查是否包含常见的编码问题字符
            return input.Contains("�") || input.Contains("Ã") || input.Contains("â");
        }

        /// <summary>
        /// 查找项目根目录
        /// </summary>
        private string FindProjectRoot(string startPath)
        {
            string currentPath = startPath;

            // 向上查找,直到找到包含pyFunc目录的路径
            while (!string.IsNullOrEmpty(currentPath))
            {
                string pyFuncPath = System.IO.Path.Combine(currentPath, "pyFunc");
                if (Directory.Exists(pyFuncPath))
                {
                    return currentPath;
                }

                // 向上一级目录
                DirectoryInfo parentDir = Directory.GetParent(currentPath);
                if (parentDir == null)
                    break;
                currentPath = parentDir.FullName;
            }

            // 如果没找到,尝试使用相对路径(开发环境)
            string devPath = System.IO.Path.GetFullPath(System.IO.Path.Combine(startPath, "..", "..", "..", ".."));
            string devPyFuncPath = System.IO.Path.Combine(devPath, "pyFunc");
            if (Directory.Exists(devPyFuncPath))
            {
                return devPath;
            }

            // 最后尝试当前工作目录
            string workingDir = Environment.CurrentDirectory;
            string workingPyFuncPath = System.IO.Path.Combine(workingDir, "pyFunc");
            if (Directory.Exists(workingPyFuncPath))
            {
                return workingDir;
            }

            throw new DirectoryNotFoundException($"无法找到pyFunc目录。搜索路径:{startPath}");
        }

        /// <summary>
        /// 运行Python脚本
        /// </summary>
        private async Task RunPythonScript(string filePath, string folderPath)
        {
            await Task.Run(() =>
            {
                try
                {
                    // 获取项目根目录(从应用程序目录向上查找)
                    string appDirectory = AppDomain.CurrentDomain.BaseDirectory;
                    string projectRoot = FindProjectRoot(appDirectory);
                    string pyFuncPath = System.IO.Path.Combine(projectRoot, "pyFunc");
                    string pythonExePath = System.IO.Path.Combine(pyFuncPath, ".venv", "Scripts", "python.exe");
                    string scriptPath = System.IO.Path.Combine(pyFuncPath, "comparison", "report.py");

                    // 检查Python可执行文件是否存在
                    if (!File.Exists(pythonExePath))
                    {
                        throw new FileNotFoundException($"Python可执行文件不存在:{pythonExePath}");
                    }

                    // 检查脚本文件是否存在
                    if (!File.Exists(scriptPath))
                    {
                        throw new FileNotFoundException($"Python脚本不存在:{scriptPath}");
                    }

                    // 更新UI(需要在UI线程中执行)
                    Dispatcher.Invoke(() =>
                    {
                        PbReportProgress.Value = 10;
                        TbProgressInfo.Text += "正在启动Python脚本...\n";
                    });

                    // 创建进程启动信息
                    string comparisonPath = System.IO.Path.Combine(pyFuncPath, "comparison");
                    ProcessStartInfo startInfo = new ProcessStartInfo
                    {
                        FileName = pythonExePath,
                        Arguments = $"\"{scriptPath}\" \"{filePath}\" \"{folderPath}\"",
                        WorkingDirectory = comparisonPath,  // 设置工作目录为comparison
                        UseShellExecute = false,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                        CreateNoWindow = true,
                        StandardOutputEncoding = Encoding.UTF8, // 使用UTF8编码
                        StandardErrorEncoding = Encoding.UTF8   // 使用UTF8编码
                    };

                    // 设置环境变量确保Python输出UTF-8
                    startInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
                    startInfo.EnvironmentVariables["PYTHONLEGACYWINDOWSSTDIO"] = "0";
                    startInfo.EnvironmentVariables["LANG"] = "zh_CN.UTF-8";

                    // 启动进程
                    using (Process process = new Process())
                    {
                        process.StartInfo = startInfo;

                        // 处理输出
                        process.OutputDataReceived += (sender, e) =>
                        {
                            if (!string.IsNullOrEmpty(e.Data))
                            {
                                Dispatcher.Invoke(() =>
                                {
                                    // 尝试修复编码问题
                                    string decodedData = FixEncoding(e.Data);
                                    TbProgressInfo.Text += decodedData + "\n";
                                    TbProgressInfo.ScrollToEnd();

                                    // 根据输出更新进度条
                                    if (decodedData.Contains("PROGRESS:"))
                                    {
                                        PbReportProgress.Value = Math.Min(PbReportProgress.Value + 15, 90);
                                    }
                                });
                            }
                        };

                        process.ErrorDataReceived += (sender, e) =>
                        {
                            if (!string.IsNullOrEmpty(e.Data))
                            {
                                Dispatcher.Invoke(() =>
                                {
                                    // 尝试修复编码问题
                                    string decodedData = FixEncoding(e.Data);
                                    TbProgressInfo.Text += $"错误:{decodedData}\n";
                                    TbProgressInfo.ScrollToEnd();
                                });
                            }
                        };

                        process.Start();
                        process.BeginOutputReadLine();
                        process.BeginErrorReadLine();

                        // 等待进程完成
                        process.WaitForExit();

                        Dispatcher.Invoke(() =>
                        {
                            if (process.ExitCode == 0)
                            {
                                TbProgressInfo.Text += "报告生成完成!\n";
                                PbReportProgress.Value = 100;
                            }
                            else
                            {
                                TbProgressInfo.Text += $"Python脚本执行失败,退出代码:{process.ExitCode}\n";
                            }
                        });
                    }
                }
                catch (Exception ex)
                {
                    Dispatcher.Invoke(() =>
                    {
                        TbProgressInfo.Text += $"执行Python脚本时发生错误:{ex.Message}\n";
                    });
                    throw;
                }
            });
        }
    }
}

其中我们特别需要关注路径问题,具体表现在:

  • python 解释器路径;
  • .py 文件路径;
  • 传参路径
  • …………

python 运行环境

我们需要在合适的地方创建 python 的虚拟环境,这里我选择的方案是
python虚拟环境.png

总结

按照我这么配置下来,应该就能成功调用 python 文件。其中涉及一些比较琐碎的声明,稍微一看即可。


RyanJoy
1 声望0 粉丝

麦当劳汉堡 我中意食麦当劳