添加项目文件。

This commit is contained in:
wangjialiang
2025-11-28 14:57:00 +08:00
parent dc322d5889
commit fca54414e6
12 changed files with 718 additions and 0 deletions

16
App.xaml Normal file
View File

@@ -0,0 +1,16 @@
<Application x:Class="WpfApp1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:local="clr-namespace:WpfApp1"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="DeepPurple" SecondaryColor="Lime" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign2.Defaults.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

14
App.xaml.cs Normal file
View File

@@ -0,0 +1,14 @@
using System.Configuration;
using System.Data;
using System.Windows;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}

10
AssemblyInfo.cs Normal file
View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

31
FolderHelper.cs Normal file
View File

@@ -0,0 +1,31 @@
using Microsoft.WindowsAPICodePack.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfApp1
{
public static class FolderHelper
{
/// <summary>
/// 打开目录选择对话框,返回选择的路径。取消则返回 null。
/// </summary>
public static string? SelectFolder(string title = "请选择保存目录")
{
var dialog = new CommonOpenFileDialog
{
IsFolderPicker = true,
Title = title
};
if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
{
return dialog.FileName;
}
return null;
}
}
}

49
MainWindow.xaml Normal file
View File

@@ -0,0 +1,49 @@
<Window x:Class="WpfApp1.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:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
xmlns:local="clr-namespace:WpfApp1"
Style="{StaticResource MaterialDesignWindow}"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
ResizeMode="NoResize"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="800">
<GroupBox>
<GroupBox.Header>
<TextBlock Text="欢迎使用" FontSize="20" Foreground="White"/>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 相机图像显示区域 -->
<Border Grid.Row="0" BorderBrush="Gray" BorderThickness="1" Margin="10">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Image Name="CameraImage" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</ScrollViewer>
</Border>
<!-- 状态显示 -->
<TextBlock Grid.Row="1" x:Name="StatusText" Margin="10" FontSize="14"
Text="正在初始化相机..." HorizontalAlignment="Center"
Foreground="DarkBlue"/>
<!-- 控制按钮 -->
<StackPanel x:Name="ButtonPanel1" Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center"
Margin="10" >
<Button x:Name="SelectCameraButton" Content="选择相机" Width="100" Height="30" Click="SelectCameraButton_Click" Margin="0,0,10,0"/>
<Button x:Name="StartCameraButton" Content="开启推流" Width="100" Height="30" Click="StartCameraButton_Click" Margin="0,0,10,0"/>
<Button x:Name="CaptureButton" Content="捕获图像" Width="100" Height="30" Click="CaptureButton_Click" Margin="0,0,10,0"/>
<Button x:Name="ChangeFolderButton" Content="更改保存文件夹" Width="150" Height="30" Click="ChangeFolderButton_Click" Margin="0,0,10,0"/>
<Button x:Name="TimedCaptureButton" Content="定时拍摄设置" Width="120" Height="30" Click="TimedCaptureButton_Click" Margin="0,0,10,0"/>
<Button x:Name="ExitButton" Content="退出" Width="100" Height="30" Click="ExitButton_Click"/>
</StackPanel>
</Grid>
</GroupBox>
</Window>

280
MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,280 @@
using Basler.Pylon;
using Microsoft.Win32;
using Microsoft.WindowsAPICodePack.Dialogs;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
namespace WpfApp1
{
public partial class MainWindow : Window
{
private Camera _camera;
private string _saveFolder = @"C:\CapturedImages";
private ICameraInfo? _selectedCameraInfo;
private bool _isCameraStarted = false;
public MainWindow()
{
InitializeComponent();
WindowStyle = WindowStyle.None;
ResizeMode = ResizeMode.NoResize;
WindowState = WindowState.Maximized;
ButtonPanel1.IsEnabled = false;
StatusText.Text = "正在初始化相机...请稍后";
Task.Run(() => InitializeCameraAsync());
}
private async Task InitializeCameraAsync()
{
try
{
var cameras = CameraFinder.Enumerate();
if (cameras.Count == 0)
{
Dispatcher.Invoke(() =>
{
ButtonPanel1.IsEnabled = true;
StatusText.Text = "未检测到相机";
});
return;
}
_selectedCameraInfo = cameras[0];
await Task.CompletedTask;
Dispatcher.Invoke(() =>
{
ButtonPanel1.IsEnabled = true;
StatusText.Text = "相机初始化完成,请选择相机并随后‘开启推流’开始取流";
StartCameraButton.Content = "开启推流";
});
}
catch (Exception ex)
{
Dispatcher.Invoke(() => MessageBox.Show("初始化失败: " + ex.Message));
}
}
private void StartCameraButton_Click(object sender, RoutedEventArgs e)
{
try
{
// 确保相机已打开
if ((_camera == null || !_camera.IsOpen) && _selectedCameraInfo != null)
{
if (_camera != null)
{
try
{
_camera.StreamGrabber.Stop();
_camera.StreamGrabber.ImageGrabbed -= OnImageGrabbed;
_camera.Close();
}
catch { }
finally { _camera = null; }
}
_camera = new Camera(_selectedCameraInfo);
_camera.Open();
_camera.Parameters[PLCamera.AcquisitionMode].SetValue(PLCamera.AcquisitionMode.Continuous);
}
if (_camera == null || !_camera.IsOpen)
{
StatusText.Text = "请先选择一个相机";
return;
}
if (!_isCameraStarted)
{
_camera.StreamGrabber.ImageGrabbed += OnImageGrabbed;
_camera.StreamGrabber.Start(GrabStrategy.LatestImages, GrabLoop.ProvidedByStreamGrabber);
_isCameraStarted = true;
StatusText.Text = "推流已开启";
StartCameraButton.Content = "关闭推流";
}
else
{
try
{
_camera.StreamGrabber.Stop();
_camera.StreamGrabber.ImageGrabbed -= OnImageGrabbed;
_isCameraStarted = false;
StatusText.Text = "推流已关闭";
StartCameraButton.Content = "开启推流";
}
catch (Exception ex)
{
StatusText.Text = "关闭推流失败:" + ex.Message;
}
}
}
catch (Exception ex)
{
StatusText.Text = "推流切换失败:" + ex.Message;
}
}
private void OnImageGrabbed(object sender, ImageGrabbedEventArgs e)
{
try
{
using (IGrabResult result = e.GrabResult)
{
if (!result.GrabSucceeded) return;
var converter = new PixelDataConverter();
converter.OutputPixelFormat = PixelType.BGRA8packed;
byte[] buffer = new byte[result.Width * result.Height * 4];
converter.Convert(buffer, result);
Dispatcher.Invoke(() =>
{
CameraImage.Source = BitmapSource.Create(
result.Width, result.Height, 96, 96,
System.Windows.Media.PixelFormats.Bgra32,
null, buffer, result.Width * 4);
});
}
}
catch
{
// 忽略异常
}
}
private void CaptureButton_Click(object sender, RoutedEventArgs e)
{
if (CameraImage.Source == null)
{
StatusText.Text = "没有图像可保存";
return;
}
try
{
string filePath = Path.Combine(_saveFolder,
$"Capture_{DateTime.Now:yyyyMMdd_HHmmss}.png");
SaveImage(CameraImage.Source as BitmapSource, filePath);
StatusText.Text = $"图像已保存到:{filePath}";
}
catch (Exception ex)
{
StatusText.Text = "保存失败:" + ex.Message;
}
}
private void SaveImage(BitmapSource bitmap, string filePath)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using (FileStream stream = new FileStream(filePath, FileMode.Create))
{
encoder.Save(stream);
}
}
private void ChangeFolderButton_Click(object sender, RoutedEventArgs e)
{
var selected = FolderHelper.SelectFolder();
if (selected != null)
{
_saveFolder = selected;
StatusText.Text = $"保存目录已切换为:{_saveFolder}";
}
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
base.OnClosing(e);
if (_camera != null)
{
try
{
_camera.StreamGrabber.Stop();
_camera.StreamGrabber.ImageGrabbed -= OnImageGrabbed;
_camera.Close();
}
catch (Exception ex)
{
StatusText.Text = "关闭相机时出错:" + ex.Message;
}
finally
{
_camera = null;
_isCameraStarted = false;
}
}
}
private void SelectCameraButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new SelectCamera();
dlg.Owner = this;
if (dlg.ShowDialog() == true)
{
var selectedInfo = dlg.SelectedCameraInfo;
_selectedCameraInfo = selectedInfo;
// 关闭旧相机,打开新相机,但不取流
try
{
if (_camera != null)
{
try
{
_camera.StreamGrabber.Stop();
_camera.StreamGrabber.ImageGrabbed -= OnImageGrabbed;
_camera.Close();
}
catch { }
finally { _camera = null; _isCameraStarted = false; }
}
_camera = new Camera(_selectedCameraInfo);
_camera.Open();
_camera.Parameters[PLCamera.AcquisitionMode].SetValue(PLCamera.AcquisitionMode.Continuous);
_isCameraStarted = false;
StatusText.Text = $"已选择并打开相机:{selectedInfo[CameraInfoKey.SerialNumber]}(未取流)。点击‘开启推流’开始/停止推流";
StartCameraButton.Content = "开启推流";
}
catch (Exception ex)
{
StatusText.Text = "打开相机失败:" + ex.Message;
}
}
}
private void ExitButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void TimedCaptureButton_Click(object sender, RoutedEventArgs e)
{
// 定时拍摄只要求相机已打开,无需取流
if (_camera == null || !_camera.IsOpen)
{
MessageBox.Show("请先选择相机(会自动打开但不取流)");
return;
}
var dlg = new TimedCapture(_camera);
dlg.Owner = this;
dlg.ShowDialog();
}
}
}

34
SelectCamera.xaml Normal file
View File

@@ -0,0 +1,34 @@
<Window x:Class="WpfApp1.SelectCamera"
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:WpfApp1"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d"
Title="选择相机" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="检测到的摄像头:" FontWeight="Bold" Margin="10" FontSize="18" Foreground="Purple"/>
<ListView x:Name="CameraList" Grid.Row="1" Margin="0 10 0 10">
<ListView.View>
<GridView>
<GridViewColumn Header="序列号" DisplayMemberBinding="{Binding Serial}"/>
<GridViewColumn Header="型号" DisplayMemberBinding="{Binding Model}"/>
<GridViewColumn Header="接口类型" DisplayMemberBinding="{Binding Interface}"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="10">
<Button Content="确认" Width="80" Margin="0 0 10 0" Click="BtnOK_Click"/>
<Button Content="取消" Width="80" Click="BtnCancel_Click"/>
</StackPanel>
</Grid>
</Window>

73
SelectCamera.xaml.cs Normal file
View File

@@ -0,0 +1,73 @@
using Basler.Pylon;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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.Shapes;
namespace WpfApp1
{
/// <summary>
/// SelectCamera.xaml 的交互逻辑
/// </summary>
public partial class SelectCamera : Window
{
public ICameraInfo SelectedCameraInfo { get; private set; }
public SelectCamera()
{
InitializeComponent();
LoadCameras();
}
private void LoadCameras()
{
CameraList.Items.Clear();
var list = CameraFinder.Enumerate();
if (list.Count == 0)
{
MessageBox.Show("未检测到任何摄像头。");
return;
}
foreach (var info in list)
{
CameraList.Items.Add(new
{
Raw = info,
Serial = info[CameraInfoKey.SerialNumber],
Model = info[CameraInfoKey.ModelName]
});
}
}
private void BtnOK_Click(object sender, RoutedEventArgs e)
{
if (CameraList.SelectedItem == null)
{
MessageBox.Show("请先选择一个相机");
return;
}
dynamic item = CameraList.SelectedItem;
SelectedCameraInfo = item.Raw;
this.DialogResult = true;
this.Close();
}
private void BtnCancel_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
this.Close();
}
}
}

44
TimedCapture.xaml Normal file
View File

@@ -0,0 +1,44 @@
<Window x:Class="WpfApp1.TimedCapture"
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:WpfApp1"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d"
Title="定时拍摄" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="10">
<TextBlock Text="定时拍摄" FontSize="20" FontWeight="Bold" Margin="0,0,0,10"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="正在使用的相机:" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBlock x:Name="CameraInfoTextBlock" Text="相机信息" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="拍摄间隔(毫秒):" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox x:Name="IntervalTextBox" Width="100"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="保存文件夹:" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox x:Name="FolderTextBox" Width="400"/>
<Button x:Name="BrowseButton" Content="浏览" Width="80" Margin="10,0,0,0" Click="BrowseButton_Click" />
</StackPanel>
<TextBlock x:Name="StatusTextBlock" Text="状态:未开始" FontSize="14" Foreground="DarkGreen" Margin="0,10,0,0"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="10">
<Button x:Name="TimedCaptureStart" Content="开始拍摄" Width="100" Margin="0,0,20,0" Click="TimedCaptureStart_Click"/>
<Button x:Name="TimedCaptureStop" Content="停止拍摄" Width="100" Click="TimedCaptureStop_Click"/>
</StackPanel>
</Grid>
</Window>

133
TimedCapture.xaml.cs Normal file
View File

@@ -0,0 +1,133 @@
using Basler.Pylon;
using System;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace WpfApp1
{
/// <summary>
/// TimedCapture.xaml 的交互逻辑
/// </summary>
public partial class TimedCapture : Window
{
private readonly Camera _camera;
private readonly DispatcherTimer _timer = new DispatcherTimer();
private string _saveFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
private int _intervalMs = 1000;
public TimedCapture(Camera camera)
{
InitializeComponent();
_camera = camera;
var name = _camera?.CameraInfo?[CameraInfoKey.FriendlyName];
CameraInfoTextBlock.Text = string.IsNullOrWhiteSpace(name) ? "未知相机" : name;
FolderTextBox.Text = _saveFolder;
IntervalTextBox.Text = _intervalMs.ToString();
_timer.Tick += OnTimerTick;
_timer.IsEnabled = false;
}
private void BrowseButton_Click(object sender, RoutedEventArgs e)
{
var selected = FolderHelper.SelectFolder("请选择保存目录");
if (!string.IsNullOrWhiteSpace(selected))
{
_saveFolder = selected;
FolderTextBox.Text = _saveFolder;
}
}
private void TimedCaptureStart_Click(object sender, RoutedEventArgs e)
{
if (_camera == null || !_camera.IsOpen)
{
MessageBox.Show("相机未打开");
return;
}
if (!int.TryParse(IntervalTextBox.Text, out var ms) || ms <= 0)
{
MessageBox.Show("请输入有效的拍摄间隔(毫秒)");
return;
}
_intervalMs = ms;
_timer.Interval = TimeSpan.FromMilliseconds(_intervalMs);
if (string.IsNullOrWhiteSpace(FolderTextBox.Text))
{
MessageBox.Show("请选择保存文件夹");
return;
}
_saveFolder = FolderTextBox.Text.Trim();
if (!Directory.Exists(_saveFolder))
{
try { Directory.CreateDirectory(_saveFolder); } catch (Exception ex) { MessageBox.Show("创建文件夹失败:" + ex.Message); return; }
}
StatusTextBlock.Text = "状态:拍摄中";
TimedCaptureStart.IsEnabled = false;
TimedCaptureStop.IsEnabled = true;
_timer.Start();
}
private void TimedCaptureStop_Click(object sender, RoutedEventArgs e)
{
_timer.Stop();
StatusTextBlock.Text = "状态:已停止";
TimedCaptureStart.IsEnabled = true;
TimedCaptureStop.IsEnabled = false;
}
private void OnTimerTick(object? sender, EventArgs e)
{
try
{
// 单次抓拍
using (IGrabResult grabResult = _camera.StreamGrabber.GrabOne(_intervalMs))
{
if (grabResult != null && grabResult.GrabSucceeded)
{
var converter = new PixelDataConverter { OutputPixelFormat = PixelType.BGRA8packed };
byte[] buffer = new byte[grabResult.Width * grabResult.Height * 4];
converter.Convert(buffer, grabResult);
var bitmap = BitmapSource.Create(
grabResult.Width,
grabResult.Height,
96, 96,
System.Windows.Media.PixelFormats.Bgra32,
null,
buffer,
grabResult.Width * 4);
string file = Path.Combine(_saveFolder, $"Timed_{DateTime.Now:yyyyMMdd_HHmmss_fff}.png");
SaveImage(bitmap, file);
StatusTextBlock.Text = "状态:已保存 " + System.IO.Path.GetFileName(file);
}
}
}
catch (Exception ex)
{
StatusTextBlock.Text = "状态:抓拍失败 - " + ex.Message;
}
}
private static void SaveImage(BitmapSource bitmap, string filePath)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
encoder.Save(stream);
}
protected override void OnClosed(EventArgs e)
{
_timer.Stop();
base.OnClosed(e);
}
}
}

25
WpfApp1.csproj Normal file
View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Basler.Pylon.NET.x64" Version="10.3.2.636" />
<PackageReference Include="MaterialDesignThemes" Version="5.3.0" />
<PackageReference Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.5" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
</ItemGroup>
<ItemGroup>
<Reference Include="Basler.Pylon">
<HintPath>..\..\..\Pylon\Development\Assemblies\Basler.Pylon\net8.0\x64\Basler.Pylon.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

9
WpfApp1.slnx Normal file
View File

@@ -0,0 +1,9 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
</Configurations>
<Project Path="WpfApp1.csproj">
<Platform Solution="*|x64" Project="x64" />
</Project>
</Solution>