Add brightness/contrast sliders, input source switching, and 9-language localization
- Add VCP commands for brightness (10), contrast (12), input source (60) - Fix UTF-16 encoding for monitor data parsing - Add system tray app with monitor controls - Add localization for en, es, fr, de, zh, ja, pt, it, hi - Update to .NET 9.0 - Add LICENSE and README 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b656ead2d
commit
0352c6b755
18
.claude/settings.local.json
Normal file
18
.claude/settings.local.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(dir \"C:\\\\Users\\\\logik\\\\source\\\\repos\\\\ControlMyMonitorManagement\\\\Language\")",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(dir \"C:\\\\Users\\\\logik\\\\source\\\\repos\\\\ControlMyMonitorManagement\\\\Language\" /S)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(dotnet build:*)",
|
||||||
|
"Bash(dotnet --list-sdks:*)",
|
||||||
|
"Bash(dotnet:*)",
|
||||||
|
"Bash(timeout /t 5 /nobreak)",
|
||||||
|
"Bash(taskkill:*)",
|
||||||
|
"Bash(start \"\" \"C:\\\\Users\\\\logik\\\\source\\\\repos\\\\ControlMyMonitorManagement\\\\DellMonitorControl\\\\bin\\\\Debug\\\\net9.0-windows\\\\DellMonitorControl.exe\")",
|
||||||
|
"Bash(ping:*)",
|
||||||
|
"Bash(git remote:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
89
CLAUDE.md
Normal file
89
CLAUDE.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
ControlMyMonitorManagement is a C# WPF desktop application for controlling monitor settings via DDC/CI (Display Data Channel/Command Interface). It allows controlling VCP (Virtual Control Panel) codes like brightness, contrast, and power state across multiple displays.
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build entire solution
|
||||||
|
dotnet build ControlMyMonitorManagement.sln
|
||||||
|
|
||||||
|
# Build specific project
|
||||||
|
dotnet build ControlMyMonitorManagement/ControlMyMonitorManagement.csproj
|
||||||
|
dotnet build DellMonitorControl/DellMonitorControl.csproj
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
dotnet test Tester/Tester.csproj
|
||||||
|
|
||||||
|
# Run main application
|
||||||
|
dotnet run --project ControlMyMonitorManagement/ControlMyMonitorManagement.csproj
|
||||||
|
|
||||||
|
# Run Dell-specific application
|
||||||
|
dotnet run --project DellMonitorControl/DellMonitorControl.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Solution Structure (7 Projects)
|
||||||
|
|
||||||
|
```
|
||||||
|
ControlMyMonitorManagement.sln
|
||||||
|
├── ControlMyMonitorManagement # Main WPF UI application
|
||||||
|
├── DellMonitorControl # Dell-specific WPF app with system tray
|
||||||
|
├── Library # Core business logic and monitor control
|
||||||
|
├── CMMModel # Data models for monitor status/VCP codes
|
||||||
|
├── CMMService # Service layer (Interface/, Service/)
|
||||||
|
├── Language # Localization support (CulturesHelper)
|
||||||
|
└── Tester # NUnit test project
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components in Library Project
|
||||||
|
|
||||||
|
- **Method/CMMMgr.cs** - Main manager class, initializes monitor collection
|
||||||
|
- **Method/CMMCommand.cs** - Core monitor control commands (PowerOn, Sleep, SetBrightness, SetContrast, SetInputSource, etc.)
|
||||||
|
- **ViewModel/XMonitor.cs** - Monitor data model (device name, adapter, serial number)
|
||||||
|
- **ViewModel/XMonitorStatus.cs** - VCP feature status (code, value, max)
|
||||||
|
- **WinAPI/Win32Api.cs** - Windows API interop for DDC/CI commands
|
||||||
|
- **Config/Config.cs** - JSON configuration management
|
||||||
|
- **Helpers/ConsoleHelper.cs** - Process wrapper for cmd.exe commands
|
||||||
|
|
||||||
|
### VCP Codes Reference
|
||||||
|
|
||||||
|
Common VCP codes used in this project:
|
||||||
|
- **D6** (Power): 1=on, 4=sleep, 5=off
|
||||||
|
- **10** (Brightness): 0-100
|
||||||
|
- **12** (Contrast): 0-100
|
||||||
|
- **60** (Input Source): 1=VGA, 3=DVI, 15=DisplayPort, 17=HDMI
|
||||||
|
|
||||||
|
## Localization
|
||||||
|
|
||||||
|
Supports 9 languages with auto-detection from system locale:
|
||||||
|
- English (en), Spanish (es), French (fr), German (de)
|
||||||
|
- Chinese (zh), Japanese (ja), Portuguese (pt), Italian (it), Hindi (hi)
|
||||||
|
|
||||||
|
Resource files: `Language/StringResources.{locale}.xaml`
|
||||||
|
Helper: `Language/CulturesHelper.cs` - call `ApplySystemLanguage()` on startup
|
||||||
|
|
||||||
|
### Embedded Resource
|
||||||
|
|
||||||
|
`Library/Resource/ControlMyMonitor.exe` - Third-party tool (by Nir Sofer) embedded for DDC/CI operations
|
||||||
|
|
||||||
|
## Patterns Used
|
||||||
|
|
||||||
|
- **MVVM** - PropertyBase class implements INotifyPropertyChanged
|
||||||
|
- **Async/Await** - Monitor scanning operations are async
|
||||||
|
- **Dependency Injection** - Used in DellMonitorControl for configuration
|
||||||
|
|
||||||
|
## Target Framework
|
||||||
|
|
||||||
|
.NET 7.0-windows with WPF (UseWPF=true)
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Dynamic version format: `Major.Minor.DaysSinceProjectStart.HourMinutes`
|
||||||
|
- Main app project start: 2017-09-17
|
||||||
|
- DellMonitorControl start: 2023-07-02
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<AssemblyName>CMM.Model</AssemblyName>
|
<AssemblyName>CMM.Model</AssemblyName>
|
||||||
<RootNamespace>CMM.Model</RootNamespace>
|
<RootNamespace>CMM.Model</RootNamespace>
|
||||||
<Product>ControlMyMonitorManagement</Product>
|
<Product>ControlMyMonitorManagement</Product>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<AssemblyName>CMM.Service</AssemblyName>
|
<AssemblyName>CMM.Service</AssemblyName>
|
||||||
<RootNamespace>CMM.Service</RootNamespace>
|
<RootNamespace>CMM.Service</RootNamespace>
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
using CMM.Library.Config;
|
using CMM.Language;
|
||||||
|
using CMM.Library.Config;
|
||||||
using CMM.Library.Method;
|
using CMM.Library.Method;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Data;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
@ -17,6 +13,7 @@ namespace CMM.Management
|
|||||||
{
|
{
|
||||||
internal static XConfig cfg { get; private set; }
|
internal static XConfig cfg { get; private set; }
|
||||||
internal static CMMMgr CMMMgr { get; private set; }
|
internal static CMMMgr CMMMgr { get; private set; }
|
||||||
|
private readonly CulturesHelper _culturesHelper = new();
|
||||||
|
|
||||||
public App()
|
public App()
|
||||||
{
|
{
|
||||||
@ -28,6 +25,7 @@ namespace CMM.Management
|
|||||||
|
|
||||||
private async Task App_Startup(object sender, StartupEventArgs e)
|
private async Task App_Startup(object sender, StartupEventArgs e)
|
||||||
{
|
{
|
||||||
|
_culturesHelper.ApplySystemLanguage();
|
||||||
await CMMMgr.Init();
|
await CMMMgr.Init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,20 @@
|
|||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using CMM.Library.ViewModel;
|
using CMM.Library.ViewModel;
|
||||||
using CMM.Library.Base;
|
|
||||||
using CMM.Library.Method;
|
using CMM.Library.Method;
|
||||||
using System.Windows.Data;
|
using CMM.Language;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CMM.Management.Control
|
namespace CMM.Management.Control
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 單一顆螢幕
|
/// Single monitor control
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class MonCtrl : System.Windows.Controls.Control
|
internal class MonCtrl : System.Windows.Controls.Control
|
||||||
{
|
{
|
||||||
public readonly static DependencyProperty MonProperty;
|
public static readonly DependencyProperty MonProperty;
|
||||||
private StackPanel _sp;
|
private StackPanel _sp;
|
||||||
|
|
||||||
static MonCtrl()
|
static MonCtrl()
|
||||||
@ -30,6 +31,10 @@ namespace CMM.Management.Control
|
|||||||
public override void OnApplyTemplate()
|
public override void OnApplyTemplate()
|
||||||
{
|
{
|
||||||
_sp = Template.FindName("sp", this) as StackPanel;
|
_sp = Template.FindName("sp", this) as StackPanel;
|
||||||
|
if (_sp != null && Mon != null)
|
||||||
|
{
|
||||||
|
_ = LoadControlsAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public XMonitor Mon
|
public XMonitor Mon
|
||||||
@ -49,7 +54,236 @@ namespace CMM.Management.Control
|
|||||||
|
|
||||||
public virtual void OnMonChanged(XMonitor value)
|
public virtual void OnMonChanged(XMonitor value)
|
||||||
{
|
{
|
||||||
|
if (_sp != null && value != null)
|
||||||
|
{
|
||||||
|
_ = LoadControlsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadControlsAsync()
|
||||||
|
{
|
||||||
|
if (_sp == null || Mon == null) return;
|
||||||
|
|
||||||
|
_sp.Children.Clear();
|
||||||
|
|
||||||
|
var brightness = await CMMCommand.GetBrightness(Mon.SerialNumber);
|
||||||
|
var contrast = await CMMCommand.GetContrast(Mon.SerialNumber);
|
||||||
|
var inputSource = await CMMCommand.GetInputSource(Mon.SerialNumber);
|
||||||
|
var inputOptions = await CMMCommand.GetInputSourceOptions(Mon.SerialNumber);
|
||||||
|
var powerStatus = await CMMCommand.GetMonPowerStatus(Mon.SerialNumber);
|
||||||
|
|
||||||
|
// Brightness slider
|
||||||
|
var brightnessPanel = CreateSliderControl(
|
||||||
|
Lang.Find("Brightness"),
|
||||||
|
brightness ?? 50,
|
||||||
|
Mon.SerialNumber,
|
||||||
|
async (sn, value) => await CMMCommand.SetBrightness(sn, value));
|
||||||
|
_sp.Children.Add(brightnessPanel);
|
||||||
|
|
||||||
|
// Contrast slider
|
||||||
|
var contrastPanel = CreateSliderControl(
|
||||||
|
Lang.Find("Contrast"),
|
||||||
|
contrast ?? 50,
|
||||||
|
Mon.SerialNumber,
|
||||||
|
async (sn, value) => await CMMCommand.SetContrast(sn, value));
|
||||||
|
_sp.Children.Add(contrastPanel);
|
||||||
|
|
||||||
|
// Input source dropdown
|
||||||
|
if (inputOptions.Count > 0)
|
||||||
|
{
|
||||||
|
var inputPanel = CreateInputSourceControl(
|
||||||
|
Mon.SerialNumber,
|
||||||
|
inputSource,
|
||||||
|
inputOptions);
|
||||||
|
_sp.Children.Add(inputPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power button
|
||||||
|
var powerPanel = CreatePowerControl(Mon.SerialNumber, powerStatus);
|
||||||
|
_sp.Children.Add(powerPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel CreateSliderControl(
|
||||||
|
string label,
|
||||||
|
int currentValue,
|
||||||
|
string monitorSN,
|
||||||
|
Func<string, int, Task> onValueChanged)
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(0, 5, 0, 5)
|
||||||
|
};
|
||||||
|
|
||||||
|
var labelBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
Width = 100,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Foreground = System.Windows.Media.Brushes.White
|
||||||
|
};
|
||||||
|
|
||||||
|
var slider = new Slider
|
||||||
|
{
|
||||||
|
Style = (Style)FindResource("Horizontal_Slider"),
|
||||||
|
Minimum = 0,
|
||||||
|
Maximum = 100,
|
||||||
|
Value = currentValue,
|
||||||
|
Tag = monitorSN,
|
||||||
|
Width = 200
|
||||||
|
};
|
||||||
|
|
||||||
|
var valueBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = currentValue.ToString(),
|
||||||
|
Width = 40,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Foreground = System.Windows.Media.Brushes.White,
|
||||||
|
TextAlignment = TextAlignment.Right
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update value display while dragging
|
||||||
|
slider.ValueChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
valueBlock.Text = ((int)e.NewValue).ToString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only send command on release (PreviewMouseUp)
|
||||||
|
slider.PreviewMouseUp += async (s, e) =>
|
||||||
|
{
|
||||||
|
var sld = s as Slider;
|
||||||
|
var sn = sld?.Tag?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(sn))
|
||||||
|
{
|
||||||
|
await onValueChanged(sn, (int)sld.Value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.Children.Add(labelBlock);
|
||||||
|
panel.Children.Add(slider);
|
||||||
|
panel.Children.Add(valueBlock);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel CreateInputSourceControl(
|
||||||
|
string monitorSN,
|
||||||
|
int? currentInputSource,
|
||||||
|
List<InputSourceOption> options)
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(0, 5, 0, 5)
|
||||||
|
};
|
||||||
|
|
||||||
|
var labelBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = Lang.Find("InputSource"),
|
||||||
|
Width = 100,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Foreground = System.Windows.Media.Brushes.White
|
||||||
|
};
|
||||||
|
|
||||||
|
var comboBox = new ComboBox
|
||||||
|
{
|
||||||
|
Width = 200,
|
||||||
|
Tag = monitorSN,
|
||||||
|
ItemsSource = options,
|
||||||
|
DisplayMemberPath = "Name"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set current selection
|
||||||
|
if (currentInputSource.HasValue)
|
||||||
|
{
|
||||||
|
var currentOption = options.Find(o => o.Value == currentInputSource.Value);
|
||||||
|
if (currentOption != null)
|
||||||
|
{
|
||||||
|
comboBox.SelectedItem = currentOption;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comboBox.SelectionChanged += async (s, e) =>
|
||||||
|
{
|
||||||
|
var cb = s as ComboBox;
|
||||||
|
var sn = cb?.Tag?.ToString();
|
||||||
|
var selectedOption = cb?.SelectedItem as InputSourceOption;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(sn) && selectedOption != null)
|
||||||
|
{
|
||||||
|
await CMMCommand.SetInputSource(sn, selectedOption.Value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.Children.Add(labelBlock);
|
||||||
|
panel.Children.Add(comboBox);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel CreatePowerControl(string monitorSN, string powerStatus)
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(0, 10, 0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
var labelBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = Lang.Find("Power"),
|
||||||
|
Width = 100,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Foreground = System.Windows.Media.Brushes.White
|
||||||
|
};
|
||||||
|
|
||||||
|
var btn = new Button
|
||||||
|
{
|
||||||
|
Tag = monitorSN,
|
||||||
|
Content = GetLocalizedPowerStatus(powerStatus),
|
||||||
|
Width = 200,
|
||||||
|
Height = 30
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.Click += async (s, e) => await TogglePower(s, e);
|
||||||
|
|
||||||
|
panel.Children.Add(labelBlock);
|
||||||
|
panel.Children.Add(btn);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLocalizedPowerStatus(string status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
"PowerOn" => Lang.Find("PowerOn"),
|
||||||
|
"Sleep" => Lang.Find("Sleep"),
|
||||||
|
"PowerOff" => Lang.Find("PowerOff"),
|
||||||
|
_ => status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TogglePower(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var btn = sender as Button;
|
||||||
|
var tag = btn?.Tag?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(tag)) return;
|
||||||
|
|
||||||
|
var currentStatus = await CMMCommand.GetMonPowerStatus(tag);
|
||||||
|
|
||||||
|
if (currentStatus == "Sleep" || currentStatus == "PowerOff")
|
||||||
|
{
|
||||||
|
await CMMCommand.PowerOn(tag);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await CMMCommand.Sleep(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
var newStatus = await CMMCommand.GetMonPowerStatus(tag);
|
||||||
|
btn!.Content = GetLocalizedPowerStatus(newStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<RootNamespace>CMM.Management</RootNamespace>
|
<RootNamespace>CMM.Management</RootNamespace>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<Copyright>Copyright © DangWang $([System.DateTime]::Now.ToString(yyyy))</Copyright>
|
<Copyright>Copyright © DangWang $([System.DateTime]::Now.ToString(yyyy))</Copyright>
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
xmlns:ctrl="clr-namespace:CMM.Management.Control"
|
xmlns:ctrl="clr-namespace:CMM.Management.Control"
|
||||||
xmlns:local="clr-namespace:CMM.Management"
|
xmlns:local="clr-namespace:CMM.Management"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="ControlMyMonitor Management"
|
Title="{DynamicResource AppTitle}"
|
||||||
Background="#666C6161"
|
Background="#666C6161"
|
||||||
AllowsTransparency="True"
|
AllowsTransparency="True"
|
||||||
ResizeMode="CanResizeWithGrip"
|
ResizeMode="CanResizeWithGrip"
|
||||||
|
|||||||
@ -1,18 +1,33 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:CMM.Management.Control">
|
xmlns:local="clr-namespace:CMM.Management.Control">
|
||||||
|
|
||||||
<Style TargetType="{x:Type local:MonCtrl}">
|
<Style TargetType="{x:Type local:MonCtrl}">
|
||||||
|
<Setter Property="Margin" Value="10"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="{x:Type local:MonCtrl}">
|
<ControlTemplate TargetType="{x:Type local:MonCtrl}">
|
||||||
<StackPanel x:Name="sp" Orientation="Horizontal" HorizontalAlignment="Left">
|
<Border Background="#44000000" CornerRadius="8" Padding="15">
|
||||||
<TextBlock Text="{Binding Mon.MonitorName, Mode=OneWay, NotifyOnSourceUpdated=True, NotifyOnValidationError=True,
|
<StackPanel Orientation="Vertical">
|
||||||
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MonCtrl}}}"/>
|
<!-- Monitor Name Header -->
|
||||||
</StackPanel>
|
<TextBlock Text="{Binding Mon.MonitorName, Mode=OneWay,
|
||||||
|
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MonCtrl}}}"
|
||||||
|
FontSize="16"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="White"
|
||||||
|
Margin="0,0,0,10"/>
|
||||||
|
<TextBlock Text="{Binding Mon.SerialNumber, Mode=OneWay,
|
||||||
|
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MonCtrl}}}"
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="#AAAAAA"
|
||||||
|
Margin="0,0,0,15"/>
|
||||||
|
<!-- Dynamic controls container -->
|
||||||
|
<StackPanel x:Name="sp" Orientation="Vertical"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@ -1,17 +1,12 @@
|
|||||||
<Application x:Class="DellMonitorControl.App"
|
<Application x:Class="DellMonitorControl.App"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:tb="clr-namespace:Hardcodet.Wpf.TaskbarNotification;assembly=Hardcodet.Wpf.TaskbarNotification.Net6"
|
||||||
xmlns:local="clr-namespace:DellMonitorControl"
|
xmlns:local="clr-namespace:DellMonitorControl"
|
||||||
StartupUri="MainWindow.xaml">
|
Startup="Application_Startup">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ResourceDictionary>
|
<tb:TaskbarIcon x:Key="TrayIcon"
|
||||||
<ResourceDictionary.MergedDictionaries>
|
IconSource="/DellLogo.ico"
|
||||||
<ResourceDictionary Source="/Style/Color.xaml"/>
|
ToolTipText="Monitor Control"/>
|
||||||
<ResourceDictionary Source="/Images/Icons.xaml"/>
|
|
||||||
<ResourceDictionary Source="/Style/Main.xaml"/>
|
|
||||||
<ResourceDictionary Source="/Style/Btn.xaml"/>
|
|
||||||
<ResourceDictionary Source="/Style/Slider.xaml"/>
|
|
||||||
</ResourceDictionary.MergedDictionaries>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
@ -1,17 +1,39 @@
|
|||||||
using System;
|
using Hardcodet.Wpf.TaskbarNotification;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Data;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace DellMonitorControl
|
namespace DellMonitorControl
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for App.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
|
private TaskbarIcon? _trayIcon;
|
||||||
|
private MainWindow? _mainWindow;
|
||||||
|
|
||||||
|
private void Application_Startup(object sender, StartupEventArgs e)
|
||||||
|
{
|
||||||
|
_trayIcon = (TaskbarIcon)FindResource("TrayIcon");
|
||||||
|
_trayIcon.TrayLeftMouseUp += TrayIcon_Click;
|
||||||
|
|
||||||
|
_mainWindow = new MainWindow();
|
||||||
|
_mainWindow.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrayIcon_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_mainWindow == null) return;
|
||||||
|
|
||||||
|
if (_mainWindow.IsVisible)
|
||||||
|
_mainWindow.Hide();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_mainWindow.Show();
|
||||||
|
_mainWindow.Activate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
_trayIcon?.Dispose();
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
<UserControl x:Class="DellMonitorControl.ControlPanel"
|
<UserControl x:Class="DellMonitorControl.ControlPanel"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
mc:Ignorable="d"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:DellMonitorControl"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
mc:Ignorable="d">
|
Width="280" Height="300">
|
||||||
<Grid>
|
<Grid Background="#333333">
|
||||||
<Border Background="{DynamicResource H3}" CornerRadius="10" MouseLeave="Border_MouseLeave"/>
|
|
||||||
<StackPanel Name="sp" Margin="10">
|
<StackPanel Name="sp" Margin="10">
|
||||||
|
<TextBlock Text="Monitor Control" Foreground="White" FontSize="16" FontWeight="Bold"/>
|
||||||
|
<TextBlock Text="Loading monitors..." Foreground="LightGray" FontSize="12" Margin="0,10,0,0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
using CMM.Library.Method;
|
using CMM.Language;
|
||||||
|
using CMM.Library.Method;
|
||||||
using CMM.Library.ViewModel;
|
using CMM.Library.ViewModel;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
@ -19,59 +23,434 @@ public partial class ControlPanel : UserControl
|
|||||||
|
|
||||||
public async Task Refresh()
|
public async Task Refresh()
|
||||||
{
|
{
|
||||||
await CMMCommand.ScanMonitor();
|
try
|
||||||
var monitors = await CMMCommand.ReadMonitorsData();
|
|
||||||
sp.Children.Clear();
|
|
||||||
|
|
||||||
foreach (var m in monitors)
|
|
||||||
{
|
{
|
||||||
var status = await CMMCommand.GetMonPowerStatus(m.SerialNumber);
|
sp.Children.Clear();
|
||||||
var ctrl = CreatControl(m, status);
|
|
||||||
sp.Children.Add(ctrl);
|
await CMMCommand.ScanMonitor();
|
||||||
|
var monitors = (await CMMCommand.ReadMonitorsData()).ToList();
|
||||||
|
|
||||||
|
if (!monitors.Any())
|
||||||
|
{
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "No DDC/CI monitors detected",
|
||||||
|
Foreground = System.Windows.Media.Brushes.White,
|
||||||
|
FontSize = 14
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var m in monitors)
|
||||||
|
{
|
||||||
|
var brightness = await CMMCommand.GetBrightness(m.SerialNumber) ?? 50;
|
||||||
|
var contrast = await CMMCommand.GetContrast(m.SerialNumber) ?? 50;
|
||||||
|
var inputSource = await CMMCommand.GetInputSource(m.SerialNumber);
|
||||||
|
var inputOptions = await CMMCommand.GetInputSourceOptions(m.SerialNumber);
|
||||||
|
var powerStatus = await CMMCommand.GetMonPowerStatus(m.SerialNumber) ?? "Unknown";
|
||||||
|
|
||||||
|
// Monitor name header
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = m.MonitorName,
|
||||||
|
Foreground = System.Windows.Media.Brushes.White,
|
||||||
|
FontSize = 14,
|
||||||
|
FontWeight = FontWeights.Bold,
|
||||||
|
Margin = new Thickness(0, 10, 0, 5)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Brightness row
|
||||||
|
var brightnessRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
|
||||||
|
brightnessRow.Children.Add(new TextBlock { Text = "Brightness", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
|
||||||
|
var bSlider = new Slider { Minimum = 0, Maximum = 100, Value = brightness, Width = 140, VerticalAlignment = VerticalAlignment.Center, Tag = m.SerialNumber };
|
||||||
|
var bValue = new TextBlock { Text = brightness.ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center };
|
||||||
|
bSlider.ValueChanged += (s, e) => bValue.Text = ((int)e.NewValue).ToString();
|
||||||
|
bSlider.PreviewMouseUp += async (s, e) => { if (s is Slider sl && sl.Tag is string sn) await CMMCommand.SetBrightness(sn, (int)sl.Value); };
|
||||||
|
brightnessRow.Children.Add(bSlider);
|
||||||
|
brightnessRow.Children.Add(bValue);
|
||||||
|
sp.Children.Add(brightnessRow);
|
||||||
|
|
||||||
|
// Contrast row
|
||||||
|
var contrastRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
|
||||||
|
contrastRow.Children.Add(new TextBlock { Text = "Contrast", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
|
||||||
|
var cSlider = new Slider { Minimum = 0, Maximum = 100, Value = contrast, Width = 140, VerticalAlignment = VerticalAlignment.Center, Tag = m.SerialNumber };
|
||||||
|
var cValue = new TextBlock { Text = contrast.ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center };
|
||||||
|
cSlider.ValueChanged += (s, e) => cValue.Text = ((int)e.NewValue).ToString();
|
||||||
|
cSlider.PreviewMouseUp += async (s, e) => { if (s is Slider sl && sl.Tag is string sn) await CMMCommand.SetContrast(sn, (int)sl.Value); };
|
||||||
|
contrastRow.Children.Add(cSlider);
|
||||||
|
contrastRow.Children.Add(cValue);
|
||||||
|
sp.Children.Add(contrastRow);
|
||||||
|
|
||||||
|
// Input source row (if options available)
|
||||||
|
if (inputOptions.Count > 0)
|
||||||
|
{
|
||||||
|
var inputRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
|
||||||
|
inputRow.Children.Add(new TextBlock { Text = "Input", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
|
||||||
|
var combo = new ComboBox { Width = 170, ItemsSource = inputOptions, DisplayMemberPath = "Name", Tag = m.SerialNumber };
|
||||||
|
if (inputSource.HasValue) combo.SelectedItem = inputOptions.Find(o => o.Value == inputSource.Value);
|
||||||
|
combo.SelectionChanged += async (s, e) => { if (s is ComboBox cb && cb.Tag is string sn && cb.SelectedItem is InputSourceOption opt) await CMMCommand.SetInputSource(sn, opt.Value); };
|
||||||
|
inputRow.Children.Add(combo);
|
||||||
|
sp.Children.Add(inputRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power row
|
||||||
|
var powerRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
|
||||||
|
powerRow.Children.Add(new TextBlock { Text = "Power", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
|
||||||
|
var powerBtn = new Button { Content = powerStatus, Width = 170, Tag = m.SerialNumber };
|
||||||
|
powerBtn.Click += async (s, e) =>
|
||||||
|
{
|
||||||
|
if (s is Button btn && btn.Tag is string sn)
|
||||||
|
{
|
||||||
|
var status = await CMMCommand.GetMonPowerStatus(sn);
|
||||||
|
if (status == "Sleep" || status == "PowerOff") await CMMCommand.PowerOn(sn);
|
||||||
|
else await CMMCommand.Sleep(sn);
|
||||||
|
await Task.Delay(1000);
|
||||||
|
btn.Content = await CMMCommand.GetMonPowerStatus(sn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
powerRow.Children.Add(powerBtn);
|
||||||
|
sp.Children.Add(powerRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
sp.Children.Clear();
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"Error: {ex.Message}",
|
||||||
|
Foreground = System.Windows.Media.Brushes.Red,
|
||||||
|
FontSize = 12,
|
||||||
|
TextWrapping = TextWrapping.Wrap
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private StackPanel CreatControl(XMonitor monitorModel, string powerStatus)
|
private StackPanel CreateControlSimple(
|
||||||
|
XMonitor monitorModel,
|
||||||
|
int? brightness,
|
||||||
|
int? contrast,
|
||||||
|
int? currentInputSource,
|
||||||
|
List<InputSourceOption> inputOptions,
|
||||||
|
string powerStatus)
|
||||||
{
|
{
|
||||||
var _sp = new StackPanel();
|
var container = new StackPanel
|
||||||
|
|
||||||
_sp.Orientation = Orientation.Vertical;
|
|
||||||
_sp.Margin = new Thickness(10, 5, 5, 0);
|
|
||||||
|
|
||||||
var tb = new TextBlock
|
|
||||||
{
|
{
|
||||||
Text = $"{monitorModel.MonitorName}({monitorModel.SerialNumber})",
|
Orientation = Orientation.Vertical,
|
||||||
|
Margin = new Thickness(0, 5, 0, 10)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Monitor name
|
||||||
|
container.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"{monitorModel.MonitorName} ({monitorModel.SerialNumber})",
|
||||||
|
Foreground = System.Windows.Media.Brushes.White,
|
||||||
|
FontSize = 14,
|
||||||
|
FontWeight = FontWeights.Bold,
|
||||||
|
Margin = new Thickness(0, 0, 0, 5)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Brightness
|
||||||
|
var brightnessPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) };
|
||||||
|
brightnessPanel.Children.Add(new TextBlock { Text = "Brightness:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 });
|
||||||
|
var brightnessSlider = new Slider { Minimum = 0, Maximum = 100, Value = brightness ?? 50, Width = 150, Tag = monitorModel.SerialNumber };
|
||||||
|
var brightnessValue = new TextBlock { Text = (brightness ?? 50).ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0) };
|
||||||
|
brightnessSlider.ValueChanged += (s, e) => brightnessValue.Text = ((int)e.NewValue).ToString();
|
||||||
|
brightnessSlider.PreviewMouseUp += async (s, e) =>
|
||||||
|
{
|
||||||
|
var sld = s as Slider;
|
||||||
|
if (sld?.Tag is string sn)
|
||||||
|
await CMMCommand.SetBrightness(sn, (int)sld.Value);
|
||||||
|
};
|
||||||
|
brightnessPanel.Children.Add(brightnessSlider);
|
||||||
|
brightnessPanel.Children.Add(brightnessValue);
|
||||||
|
container.Children.Add(brightnessPanel);
|
||||||
|
|
||||||
|
// Contrast
|
||||||
|
var contrastPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) };
|
||||||
|
contrastPanel.Children.Add(new TextBlock { Text = "Contrast:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 });
|
||||||
|
var contrastSlider = new Slider { Minimum = 0, Maximum = 100, Value = contrast ?? 50, Width = 150, Tag = monitorModel.SerialNumber };
|
||||||
|
var contrastValue = new TextBlock { Text = (contrast ?? 50).ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0) };
|
||||||
|
contrastSlider.ValueChanged += (s, e) => contrastValue.Text = ((int)e.NewValue).ToString();
|
||||||
|
contrastSlider.PreviewMouseUp += async (s, e) =>
|
||||||
|
{
|
||||||
|
var sld = s as Slider;
|
||||||
|
if (sld?.Tag is string sn)
|
||||||
|
await CMMCommand.SetContrast(sn, (int)sld.Value);
|
||||||
|
};
|
||||||
|
contrastPanel.Children.Add(contrastSlider);
|
||||||
|
contrastPanel.Children.Add(contrastValue);
|
||||||
|
container.Children.Add(contrastPanel);
|
||||||
|
|
||||||
|
// Input source (if available)
|
||||||
|
if (inputOptions.Count > 0)
|
||||||
|
{
|
||||||
|
var inputPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) };
|
||||||
|
inputPanel.Children.Add(new TextBlock { Text = "Input:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 });
|
||||||
|
var combo = new ComboBox { Width = 150, ItemsSource = inputOptions, DisplayMemberPath = "Name", Tag = monitorModel.SerialNumber };
|
||||||
|
if (currentInputSource.HasValue)
|
||||||
|
combo.SelectedItem = inputOptions.Find(o => o.Value == currentInputSource.Value);
|
||||||
|
combo.SelectionChanged += async (s, e) =>
|
||||||
|
{
|
||||||
|
var cb = s as ComboBox;
|
||||||
|
if (cb?.Tag is string sn && cb.SelectedItem is InputSourceOption opt)
|
||||||
|
await CMMCommand.SetInputSource(sn, opt.Value);
|
||||||
|
};
|
||||||
|
inputPanel.Children.Add(combo);
|
||||||
|
container.Children.Add(inputPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power
|
||||||
|
var powerPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) };
|
||||||
|
powerPanel.Children.Add(new TextBlock { Text = "Power:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 });
|
||||||
|
var powerBtn = new Button { Content = powerStatus, Width = 150, Tag = monitorModel.SerialNumber };
|
||||||
|
powerBtn.Click += async (s, e) =>
|
||||||
|
{
|
||||||
|
var btn = s as Button;
|
||||||
|
if (btn?.Tag is string sn)
|
||||||
|
{
|
||||||
|
var status = await CMMCommand.GetMonPowerStatus(sn);
|
||||||
|
if (status == "Sleep" || status == "PowerOff")
|
||||||
|
await CMMCommand.PowerOn(sn);
|
||||||
|
else
|
||||||
|
await CMMCommand.Sleep(sn);
|
||||||
|
await Task.Delay(1000);
|
||||||
|
btn.Content = await CMMCommand.GetMonPowerStatus(sn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
powerPanel.Children.Add(powerBtn);
|
||||||
|
container.Children.Add(powerPanel);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel CreateControl(
|
||||||
|
XMonitor monitorModel,
|
||||||
|
int? brightness,
|
||||||
|
int? contrast,
|
||||||
|
int? currentInputSource,
|
||||||
|
List<InputSourceOption> inputOptions,
|
||||||
|
string powerStatus)
|
||||||
|
{
|
||||||
|
var container = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Vertical,
|
||||||
|
Margin = new Thickness(10, 5, 10, 10)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Monitor name header
|
||||||
|
var header = new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"{monitorModel.MonitorName} ({monitorModel.SerialNumber})",
|
||||||
HorizontalAlignment = HorizontalAlignment.Left,
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
|
Style = (Style)FindResource("LableStyle"),
|
||||||
|
Margin = new Thickness(0, 0, 0, 5)
|
||||||
|
};
|
||||||
|
container.Children.Add(header);
|
||||||
|
|
||||||
|
// Brightness slider
|
||||||
|
var brightnessPanel = CreateSliderControl(
|
||||||
|
Lang.Find("Brightness"),
|
||||||
|
brightness ?? 50,
|
||||||
|
monitorModel.SerialNumber,
|
||||||
|
async (sn, value) => await CMMCommand.SetBrightness(sn, value));
|
||||||
|
container.Children.Add(brightnessPanel);
|
||||||
|
|
||||||
|
// Contrast slider
|
||||||
|
var contrastPanel = CreateSliderControl(
|
||||||
|
Lang.Find("Contrast"),
|
||||||
|
contrast ?? 50,
|
||||||
|
monitorModel.SerialNumber,
|
||||||
|
async (sn, value) => await CMMCommand.SetContrast(sn, value));
|
||||||
|
container.Children.Add(contrastPanel);
|
||||||
|
|
||||||
|
// Input source dropdown
|
||||||
|
if (inputOptions.Count > 0)
|
||||||
|
{
|
||||||
|
var inputPanel = CreateInputSourceControl(
|
||||||
|
monitorModel.SerialNumber,
|
||||||
|
currentInputSource,
|
||||||
|
inputOptions);
|
||||||
|
container.Children.Add(inputPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power button
|
||||||
|
var powerPanel = CreatePowerControl(monitorModel.SerialNumber, powerStatus);
|
||||||
|
container.Children.Add(powerPanel);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel CreateSliderControl(
|
||||||
|
string label,
|
||||||
|
int currentValue,
|
||||||
|
string monitorSN,
|
||||||
|
Func<string, int, Task> onValueChanged)
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(0, 3, 0, 3)
|
||||||
|
};
|
||||||
|
|
||||||
|
var labelBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
Width = 80,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Style = (Style)FindResource("LableStyle")
|
||||||
|
};
|
||||||
|
|
||||||
|
var slider = new Slider
|
||||||
|
{
|
||||||
|
Style = (Style)FindResource("Horizontal_Slider"),
|
||||||
|
Minimum = 0,
|
||||||
|
Maximum = 100,
|
||||||
|
Value = currentValue,
|
||||||
|
Tag = monitorSN,
|
||||||
|
Width = 180
|
||||||
|
};
|
||||||
|
|
||||||
|
var valueBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = currentValue.ToString(),
|
||||||
|
Width = 35,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Style = (Style)FindResource("LableStyle"),
|
||||||
|
TextAlignment = TextAlignment.Right
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update value display while dragging
|
||||||
|
slider.ValueChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
valueBlock.Text = ((int)e.NewValue).ToString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only send command on release (PreviewMouseUp)
|
||||||
|
slider.PreviewMouseUp += async (s, e) =>
|
||||||
|
{
|
||||||
|
var sld = s as Slider;
|
||||||
|
var sn = sld?.Tag?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(sn) && sld != null)
|
||||||
|
{
|
||||||
|
await onValueChanged(sn, (int)sld.Value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.Children.Add(labelBlock);
|
||||||
|
panel.Children.Add(slider);
|
||||||
|
panel.Children.Add(valueBlock);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel CreateInputSourceControl(
|
||||||
|
string monitorSN,
|
||||||
|
int? currentInputSource,
|
||||||
|
List<InputSourceOption> options)
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(0, 3, 0, 3)
|
||||||
|
};
|
||||||
|
|
||||||
|
var labelBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = Lang.Find("InputSource"),
|
||||||
|
Width = 80,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Style = (Style)FindResource("LableStyle")
|
||||||
|
};
|
||||||
|
|
||||||
|
var comboBox = new ComboBox
|
||||||
|
{
|
||||||
|
Width = 180,
|
||||||
|
Tag = monitorSN,
|
||||||
|
ItemsSource = options,
|
||||||
|
DisplayMemberPath = "Name"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set current selection
|
||||||
|
if (currentInputSource.HasValue)
|
||||||
|
{
|
||||||
|
var currentOption = options.Find(o => o.Value == currentInputSource.Value);
|
||||||
|
if (currentOption != null)
|
||||||
|
{
|
||||||
|
comboBox.SelectedItem = currentOption;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comboBox.SelectionChanged += async (s, e) =>
|
||||||
|
{
|
||||||
|
var cb = s as ComboBox;
|
||||||
|
var sn = cb?.Tag?.ToString();
|
||||||
|
var selectedOption = cb?.SelectedItem as InputSourceOption;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(sn) && selectedOption != null)
|
||||||
|
{
|
||||||
|
await CMMCommand.SetInputSource(sn, selectedOption.Value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.Children.Add(labelBlock);
|
||||||
|
panel.Children.Add(comboBox);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel CreatePowerControl(string monitorSN, string powerStatus)
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(0, 5, 0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
var labelBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = Lang.Find("Power"),
|
||||||
|
Width = 80,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
Style = (Style)FindResource("LableStyle")
|
Style = (Style)FindResource("LableStyle")
|
||||||
};
|
};
|
||||||
|
|
||||||
var btn = new Button
|
var btn = new Button
|
||||||
{
|
{
|
||||||
Tag = monitorModel.SerialNumber,
|
Tag = monitorSN,
|
||||||
Content = powerStatus,
|
Content = GetLocalizedPowerStatus(powerStatus),
|
||||||
Style = (Style)FindResource("TextButtonStyle")
|
Style = (Style)FindResource("TextButtonStyle"),
|
||||||
|
Width = 180
|
||||||
};
|
};
|
||||||
|
|
||||||
btn.Click += async (s, e) => await ToggleButton_Checked(s, e);
|
btn.Click += async (s, e) => await TogglePower(s, e);
|
||||||
|
|
||||||
_sp.Children.Add(tb);
|
panel.Children.Add(labelBlock);
|
||||||
_sp.Children.Add(btn);
|
panel.Children.Add(btn);
|
||||||
|
|
||||||
return _sp;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Border_MouseLeave(object sender, MouseEventArgs e)
|
private string GetLocalizedPowerStatus(string status)
|
||||||
{
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
"PowerOn" => Lang.Find("PowerOn"),
|
||||||
|
"Sleep" => Lang.Find("Sleep"),
|
||||||
|
"PowerOff" => Lang.Find("PowerOff"),
|
||||||
|
_ => status
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ToggleButton_Checked(object sender, RoutedEventArgs e)
|
private async Task TogglePower(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var btn = sender as Button;
|
var btn = sender as Button;
|
||||||
var tag = btn?.Tag.ToString();
|
var tag = btn?.Tag?.ToString();
|
||||||
var content = btn?.Content as string;
|
if (string.IsNullOrEmpty(tag)) return;
|
||||||
if (content == "Sleep")
|
|
||||||
{
|
var currentStatus = await CMMCommand.GetMonPowerStatus(tag);
|
||||||
|
|
||||||
|
if (currentStatus == "Sleep" || currentStatus == "PowerOff")
|
||||||
|
{
|
||||||
await CMMCommand.PowerOn(tag);
|
await CMMCommand.PowerOn(tag);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -80,6 +459,34 @@ public partial class ControlPanel : UserControl
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
btn!.Content = await CMMCommand.GetMonPowerStatus(tag);
|
var newStatus = await CMMCommand.GetMonPowerStatus(tag);
|
||||||
|
btn!.Content = GetLocalizedPowerStatus(newStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Border_MouseLeave(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// Use Dispatcher to let the popup fully render first
|
||||||
|
Dispatcher.BeginInvoke(new Action(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Refresh();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
sp.Children.Clear();
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"Error: {ex.Message}",
|
||||||
|
Foreground = System.Windows.Media.Brushes.Red,
|
||||||
|
FontSize = 12,
|
||||||
|
TextWrapping = TextWrapping.Wrap
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AssemblyName>DellMonitorControl</AssemblyName>
|
<AssemblyName>DellMonitorControl</AssemblyName>
|
||||||
<RootNamespace>DellMonitorControl</RootNamespace>
|
<RootNamespace>DellMonitorControl</RootNamespace>
|
||||||
@ -45,6 +45,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Library\Library.csproj" />
|
<ProjectReference Include="..\Library\Library.csproj" />
|
||||||
|
<ProjectReference Include="..\Language\Language.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,21 +1,15 @@
|
|||||||
<Window x:Class="DellMonitorControl.MainWindow"
|
<Window x:Class="DellMonitorControl.MainWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
Title="Monitor Control" Height="400" Width="320"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
WindowStyle="ToolWindow"
|
||||||
xmlns:tb="clr-namespace:Hardcodet.Wpf.TaskbarNotification;assembly=Hardcodet.Wpf.TaskbarNotification.Net6"
|
ResizeMode="CanResize"
|
||||||
xmlns:local="clr-namespace:DellMonitorControl"
|
Background="#333333">
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="MainWindow" Height="450" Width="800">
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Margin="10">
|
||||||
|
<StackPanel Name="sp">
|
||||||
<Grid>
|
<TextBlock Text="Monitor Control" Foreground="White" FontSize="16" FontWeight="Bold"/>
|
||||||
<tb:TaskbarIcon x:Name="taskbar"
|
<TextBlock Text="Loading..." Foreground="LightGray" FontSize="12" Margin="0,10,0,0"/>
|
||||||
IconSource="/DellLogo.ico"
|
</StackPanel>
|
||||||
ToolTipText="螢幕開關"
|
</ScrollViewer>
|
||||||
PopupActivation="LeftOrDoubleClick">
|
|
||||||
<tb:TaskbarIcon.TrayPopup>
|
|
||||||
<local:ControlPanel x:Name="comtrolPanel"/>
|
|
||||||
</tb:TaskbarIcon.TrayPopup>
|
|
||||||
</tb:TaskbarIcon>
|
|
||||||
</Grid>
|
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@ -1,22 +1,171 @@
|
|||||||
using System.Threading.Tasks;
|
using CMM.Library.Method;
|
||||||
|
using CMM.Library.ViewModel;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
namespace DellMonitorControl;
|
namespace DellMonitorControl;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for MainWindow.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
this.Hide();
|
PositionWindowNearTray();
|
||||||
taskbar.TrayPopupOpen += async (s, e) => await Taskbar_TrayPopupOpen(s, e);
|
Loaded += async (s, e) => await LoadMonitors();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Taskbar_TrayPopupOpen(object sender, RoutedEventArgs e)
|
private void PositionWindowNearTray()
|
||||||
{
|
{
|
||||||
await comtrolPanel.Refresh();
|
var workArea = SystemParameters.WorkArea;
|
||||||
|
Left = workArea.Right - Width - 10;
|
||||||
|
Top = workArea.Bottom - Height - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string LogFile = System.IO.Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "MonitorControl.log");
|
||||||
|
|
||||||
|
private static void Log(string msg)
|
||||||
|
{
|
||||||
|
try { System.IO.File.AppendAllText(LogFile, $"{DateTime.Now:HH:mm:ss.fff} {msg}\n"); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadMonitors()
|
||||||
|
{
|
||||||
|
var newChildren = new List<UIElement>();
|
||||||
|
Log("LoadMonitors started");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log("Calling ScanMonitor...");
|
||||||
|
await CMMCommand.ScanMonitor();
|
||||||
|
Log("ScanMonitor complete");
|
||||||
|
|
||||||
|
Log("Calling ReadMonitorsData...");
|
||||||
|
var monitors = (await CMMCommand.ReadMonitorsData()).ToList();
|
||||||
|
Log($"ReadMonitorsData complete, found {monitors.Count} monitors");
|
||||||
|
|
||||||
|
if (!monitors.Any())
|
||||||
|
{
|
||||||
|
Log("No monitors found");
|
||||||
|
newChildren.Add(new TextBlock { Text = "No DDC/CI monitors detected", Foreground = Brushes.White, FontSize = 14 });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var m in monitors)
|
||||||
|
{
|
||||||
|
Log($"Processing monitor: {m.MonitorName} ({m.SerialNumber})");
|
||||||
|
|
||||||
|
var brightness = await CMMCommand.GetBrightness(m.SerialNumber) ?? 50;
|
||||||
|
Log($" Brightness: {brightness}");
|
||||||
|
var contrast = await CMMCommand.GetContrast(m.SerialNumber) ?? 50;
|
||||||
|
Log($" Contrast: {contrast}");
|
||||||
|
var inputSource = await CMMCommand.GetInputSource(m.SerialNumber);
|
||||||
|
Log($" InputSource: {inputSource}");
|
||||||
|
var inputOptions = await CMMCommand.GetInputSourceOptions(m.SerialNumber);
|
||||||
|
Log($" InputOptions count: {inputOptions.Count}");
|
||||||
|
var powerStatus = await CMMCommand.GetMonPowerStatus(m.SerialNumber) ?? "Unknown";
|
||||||
|
Log($" PowerStatus: {powerStatus}");
|
||||||
|
|
||||||
|
newChildren.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = m.MonitorName,
|
||||||
|
Foreground = Brushes.White,
|
||||||
|
FontSize = 14,
|
||||||
|
FontWeight = FontWeights.Bold,
|
||||||
|
Margin = new Thickness(0, 10, 0, 5)
|
||||||
|
});
|
||||||
|
|
||||||
|
newChildren.Add(CreateSliderRow("Brightness", brightness, m.SerialNumber, CMMCommand.SetBrightness));
|
||||||
|
newChildren.Add(CreateSliderRow("Contrast", contrast, m.SerialNumber, CMMCommand.SetContrast));
|
||||||
|
|
||||||
|
if (inputOptions.Count > 0)
|
||||||
|
newChildren.Add(CreateInputRow(inputSource, inputOptions, m.SerialNumber));
|
||||||
|
|
||||||
|
newChildren.Add(CreatePowerRow(powerStatus, m.SerialNumber));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log($"Built {newChildren.Count} UI elements");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"ERROR: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}");
|
||||||
|
newChildren.Clear();
|
||||||
|
newChildren.Add(new TextBlock { Text = $"Error: {ex.Message}", Foreground = Brushes.Red, FontSize = 12, TextWrapping = TextWrapping.Wrap });
|
||||||
|
}
|
||||||
|
|
||||||
|
Log("Updating UI...");
|
||||||
|
sp.Children.Clear();
|
||||||
|
foreach (var child in newChildren)
|
||||||
|
sp.Children.Add(child);
|
||||||
|
Log($"UI updated with {sp.Children.Count} children");
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel CreateSliderRow(string label, int value, string serialNumber, Func<string, int, Task> setCommand)
|
||||||
|
{
|
||||||
|
var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
|
||||||
|
row.Children.Add(new TextBlock { Text = label, Foreground = Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
|
||||||
|
|
||||||
|
var slider = new Slider { Minimum = 0, Maximum = 100, Value = value, Width = 140, Tag = serialNumber };
|
||||||
|
var valueText = new TextBlock { Text = value.ToString(), Foreground = Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0) };
|
||||||
|
|
||||||
|
slider.ValueChanged += (s, e) => valueText.Text = ((int)e.NewValue).ToString();
|
||||||
|
slider.PreviewMouseUp += async (s, e) =>
|
||||||
|
{
|
||||||
|
if (s is Slider sl && sl.Tag is string sn)
|
||||||
|
await setCommand(sn, (int)sl.Value);
|
||||||
|
};
|
||||||
|
|
||||||
|
row.Children.Add(slider);
|
||||||
|
row.Children.Add(valueText);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel CreateInputRow(int? currentInput, List<InputSourceOption> options, string serialNumber)
|
||||||
|
{
|
||||||
|
var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
|
||||||
|
row.Children.Add(new TextBlock { Text = "Input", Foreground = Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
|
||||||
|
|
||||||
|
var combo = new ComboBox { Width = 170, ItemsSource = options, DisplayMemberPath = "Name", Tag = serialNumber };
|
||||||
|
if (currentInput.HasValue)
|
||||||
|
combo.SelectedItem = options.Find(o => o.Value == currentInput.Value);
|
||||||
|
|
||||||
|
combo.SelectionChanged += async (s, e) =>
|
||||||
|
{
|
||||||
|
if (s is ComboBox cb && cb.Tag is string sn && cb.SelectedItem is InputSourceOption opt)
|
||||||
|
await CMMCommand.SetInputSource(sn, opt.Value);
|
||||||
|
};
|
||||||
|
|
||||||
|
row.Children.Add(combo);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel CreatePowerRow(string status, string serialNumber)
|
||||||
|
{
|
||||||
|
var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
|
||||||
|
row.Children.Add(new TextBlock { Text = "Power", Foreground = Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
|
||||||
|
|
||||||
|
var btn = new Button { Content = status, Width = 170, Tag = serialNumber };
|
||||||
|
btn.Click += async (s, e) =>
|
||||||
|
{
|
||||||
|
if (s is Button b && b.Tag is string sn)
|
||||||
|
{
|
||||||
|
var current = await CMMCommand.GetMonPowerStatus(sn);
|
||||||
|
if (current == "Sleep" || current == "PowerOff")
|
||||||
|
await CMMCommand.PowerOn(sn);
|
||||||
|
else
|
||||||
|
await CMMCommand.Sleep(sn);
|
||||||
|
await Task.Delay(1000);
|
||||||
|
b.Content = await CMMCommand.GetMonPowerStatus(sn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
row.Children.Add(btn);
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
LICENSE
Normal file
37
LICENSE
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
MIT License (for new contributions only)
|
||||||
|
|
||||||
|
Copyright (c) 2026 David H Friedel Jr
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
NOTICE: This project contains components with different licensing terms:
|
||||||
|
|
||||||
|
1. Original code by DangWang - No explicit license was provided with the
|
||||||
|
original repository. Use at your own discretion.
|
||||||
|
|
||||||
|
2. ControlMyMonitor.exe by Nir Sofer (NirSoft) - Freeware license:
|
||||||
|
- Free distribution allowed
|
||||||
|
- No commercial use or sale
|
||||||
|
- No modification
|
||||||
|
- See: https://www.nirsoft.net
|
||||||
|
|
||||||
|
3. New contributions (brightness/contrast controls, input source switching,
|
||||||
|
9-language localization) - MIT License as above.
|
||||||
@ -54,12 +54,48 @@ namespace CMM.Language
|
|||||||
var Languages = new List<string>();
|
var Languages = new List<string>();
|
||||||
string uriPath = ResourceLocalPath + "/" + LanguageFileName;
|
string uriPath = ResourceLocalPath + "/" + LanguageFileName;
|
||||||
|
|
||||||
Languages.Add(uriPath + ".en-US.xaml");
|
Languages.Add(uriPath + ".en.xaml");
|
||||||
Languages.Add(uriPath + ".zh-TW.xaml");
|
Languages.Add(uriPath + ".es.xaml");
|
||||||
|
Languages.Add(uriPath + ".fr.xaml");
|
||||||
|
Languages.Add(uriPath + ".de.xaml");
|
||||||
|
Languages.Add(uriPath + ".zh.xaml");
|
||||||
|
Languages.Add(uriPath + ".ja.xaml");
|
||||||
|
Languages.Add(uriPath + ".pt.xaml");
|
||||||
|
Languages.Add(uriPath + ".it.xaml");
|
||||||
|
Languages.Add(uriPath + ".hi.xaml");
|
||||||
|
|
||||||
return Languages;
|
return Languages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auto-detect and apply system language
|
||||||
|
/// </summary>
|
||||||
|
public void ApplySystemLanguage()
|
||||||
|
{
|
||||||
|
var systemCulture = CultureInfo.CurrentUICulture;
|
||||||
|
var matchedCulture = FindBestMatchingCulture(systemCulture);
|
||||||
|
ChangeCulture(matchedCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find the best matching supported culture for the given culture
|
||||||
|
/// </summary>
|
||||||
|
private CultureInfo FindBestMatchingCulture(CultureInfo culture)
|
||||||
|
{
|
||||||
|
// Try exact match first
|
||||||
|
var exactMatch = SupportedCultures.FirstOrDefault(c =>
|
||||||
|
c.Name.Equals(culture.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (exactMatch != null) return exactMatch;
|
||||||
|
|
||||||
|
// Try matching by two-letter ISO language name
|
||||||
|
var languageMatch = SupportedCultures.FirstOrDefault(c =>
|
||||||
|
c.TwoLetterISOLanguageName.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (languageMatch != null) return languageMatch;
|
||||||
|
|
||||||
|
// Default to English
|
||||||
|
return SupportedCultures.FirstOrDefault(c => c.Name == "en") ?? SupportedCultures.First();
|
||||||
|
}
|
||||||
|
|
||||||
public void ChangeCulture(string cultureName)
|
public void ChangeCulture(string cultureName)
|
||||||
{
|
{
|
||||||
var cultureInfo = CultureInfo.GetCultureInfo(cultureName);
|
var cultureInfo = CultureInfo.GetCultureInfo(cultureName);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<RootNamespace>CMM.Language</RootNamespace>
|
<RootNamespace>CMM.Language</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
@ -22,13 +22,34 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="StringResources.en-US.xaml">
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</None>
|
|
||||||
<None Update="StringResources.xaml">
|
<None Update="StringResources.xaml">
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</None>
|
</None>
|
||||||
<None Update="StringResources.zh-TW.xam">
|
<None Update="StringResources.en.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</None>
|
||||||
|
<None Update="StringResources.es.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</None>
|
||||||
|
<None Update="StringResources.fr.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</None>
|
||||||
|
<None Update="StringResources.de.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</None>
|
||||||
|
<None Update="StringResources.zh.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</None>
|
||||||
|
<None Update="StringResources.ja.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</None>
|
||||||
|
<None Update="StringResources.pt.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</None>
|
||||||
|
<None Update="StringResources.it.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</None>
|
||||||
|
<None Update="StringResources.hi.xaml">
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
29
Language/StringResources.de.xaml
Normal file
29
Language/StringResources.de.xaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
|
<!-- Application -->
|
||||||
|
<sys:String x:Key="AppTitle">Monitorsteuerung</sys:String>
|
||||||
|
<sys:String x:Key="ScreenSwitch">Bildschirmschalter</sys:String>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<sys:String x:Key="Brightness">Helligkeit</sys:String>
|
||||||
|
<sys:String x:Key="Contrast">Kontrast</sys:String>
|
||||||
|
<sys:String x:Key="InputSource">Eingangsquelle</sys:String>
|
||||||
|
<sys:String x:Key="Power">Stromversorgung</sys:String>
|
||||||
|
|
||||||
|
<!-- Power States -->
|
||||||
|
<sys:String x:Key="PowerOn">Einschalten</sys:String>
|
||||||
|
<sys:String x:Key="Sleep">Ruhezustand</sys:String>
|
||||||
|
<sys:String x:Key="PowerOff">Ausschalten</sys:String>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<sys:String x:Key="Minimize">Minimieren</sys:String>
|
||||||
|
<sys:String x:Key="Exit">Beenden</sys:String>
|
||||||
|
<sys:String x:Key="Loading">Laden...</sys:String>
|
||||||
|
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<sys:String x:Key="LoadConfigErr">Fehler beim Laden der Konfiguration: </sys:String>
|
||||||
|
<sys:String x:Key="SaveConfigErr">Fehler beim Speichern der Konfiguration: </sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
29
Language/StringResources.en.xaml
Normal file
29
Language/StringResources.en.xaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
|
<!-- Application -->
|
||||||
|
<sys:String x:Key="AppTitle">Monitor Control Management</sys:String>
|
||||||
|
<sys:String x:Key="ScreenSwitch">Screen Switch</sys:String>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<sys:String x:Key="Brightness">Brightness</sys:String>
|
||||||
|
<sys:String x:Key="Contrast">Contrast</sys:String>
|
||||||
|
<sys:String x:Key="InputSource">Input Source</sys:String>
|
||||||
|
<sys:String x:Key="Power">Power</sys:String>
|
||||||
|
|
||||||
|
<!-- Power States -->
|
||||||
|
<sys:String x:Key="PowerOn">Power On</sys:String>
|
||||||
|
<sys:String x:Key="Sleep">Sleep</sys:String>
|
||||||
|
<sys:String x:Key="PowerOff">Power Off</sys:String>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<sys:String x:Key="Minimize">Minimize</sys:String>
|
||||||
|
<sys:String x:Key="Exit">Exit</sys:String>
|
||||||
|
<sys:String x:Key="Loading">Loading...</sys:String>
|
||||||
|
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<sys:String x:Key="LoadConfigErr">Failed to load configuration: </sys:String>
|
||||||
|
<sys:String x:Key="SaveConfigErr">Failed to save configuration: </sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
29
Language/StringResources.es.xaml
Normal file
29
Language/StringResources.es.xaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
|
<!-- Application -->
|
||||||
|
<sys:String x:Key="AppTitle">Control de Monitor</sys:String>
|
||||||
|
<sys:String x:Key="ScreenSwitch">Interruptor de Pantalla</sys:String>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<sys:String x:Key="Brightness">Brillo</sys:String>
|
||||||
|
<sys:String x:Key="Contrast">Contraste</sys:String>
|
||||||
|
<sys:String x:Key="InputSource">Fuente de Entrada</sys:String>
|
||||||
|
<sys:String x:Key="Power">Encendido</sys:String>
|
||||||
|
|
||||||
|
<!-- Power States -->
|
||||||
|
<sys:String x:Key="PowerOn">Encender</sys:String>
|
||||||
|
<sys:String x:Key="Sleep">Suspender</sys:String>
|
||||||
|
<sys:String x:Key="PowerOff">Apagar</sys:String>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<sys:String x:Key="Minimize">Minimizar</sys:String>
|
||||||
|
<sys:String x:Key="Exit">Salir</sys:String>
|
||||||
|
<sys:String x:Key="Loading">Cargando...</sys:String>
|
||||||
|
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<sys:String x:Key="LoadConfigErr">Error al cargar la configuración: </sys:String>
|
||||||
|
<sys:String x:Key="SaveConfigErr">Error al guardar la configuración: </sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
29
Language/StringResources.fr.xaml
Normal file
29
Language/StringResources.fr.xaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
|
<!-- Application -->
|
||||||
|
<sys:String x:Key="AppTitle">Gestion du Moniteur</sys:String>
|
||||||
|
<sys:String x:Key="ScreenSwitch">Interrupteur d'Écran</sys:String>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<sys:String x:Key="Brightness">Luminosité</sys:String>
|
||||||
|
<sys:String x:Key="Contrast">Contraste</sys:String>
|
||||||
|
<sys:String x:Key="InputSource">Source d'Entrée</sys:String>
|
||||||
|
<sys:String x:Key="Power">Alimentation</sys:String>
|
||||||
|
|
||||||
|
<!-- Power States -->
|
||||||
|
<sys:String x:Key="PowerOn">Allumer</sys:String>
|
||||||
|
<sys:String x:Key="Sleep">Veille</sys:String>
|
||||||
|
<sys:String x:Key="PowerOff">Éteindre</sys:String>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<sys:String x:Key="Minimize">Réduire</sys:String>
|
||||||
|
<sys:String x:Key="Exit">Quitter</sys:String>
|
||||||
|
<sys:String x:Key="Loading">Chargement...</sys:String>
|
||||||
|
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<sys:String x:Key="LoadConfigErr">Échec du chargement de la configuration: </sys:String>
|
||||||
|
<sys:String x:Key="SaveConfigErr">Échec de la sauvegarde de la configuration: </sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
29
Language/StringResources.hi.xaml
Normal file
29
Language/StringResources.hi.xaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
|
<!-- Application -->
|
||||||
|
<sys:String x:Key="AppTitle">मॉनिटर नियंत्रण प्रबंधन</sys:String>
|
||||||
|
<sys:String x:Key="ScreenSwitch">स्क्रीन स्विच</sys:String>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<sys:String x:Key="Brightness">चमक</sys:String>
|
||||||
|
<sys:String x:Key="Contrast">कंट्रास्ट</sys:String>
|
||||||
|
<sys:String x:Key="InputSource">इनपुट स्रोत</sys:String>
|
||||||
|
<sys:String x:Key="Power">पावर</sys:String>
|
||||||
|
|
||||||
|
<!-- Power States -->
|
||||||
|
<sys:String x:Key="PowerOn">चालू करें</sys:String>
|
||||||
|
<sys:String x:Key="Sleep">स्लीप</sys:String>
|
||||||
|
<sys:String x:Key="PowerOff">बंद करें</sys:String>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<sys:String x:Key="Minimize">छोटा करें</sys:String>
|
||||||
|
<sys:String x:Key="Exit">बाहर निकलें</sys:String>
|
||||||
|
<sys:String x:Key="Loading">लोड हो रहा है...</sys:String>
|
||||||
|
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<sys:String x:Key="LoadConfigErr">कॉन्फ़िगरेशन लोड करने में विफल: </sys:String>
|
||||||
|
<sys:String x:Key="SaveConfigErr">कॉन्फ़िगरेशन सहेजने में विफल: </sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
29
Language/StringResources.it.xaml
Normal file
29
Language/StringResources.it.xaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
|
<!-- Application -->
|
||||||
|
<sys:String x:Key="AppTitle">Gestione Monitor</sys:String>
|
||||||
|
<sys:String x:Key="ScreenSwitch">Interruttore Schermo</sys:String>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<sys:String x:Key="Brightness">Luminosità</sys:String>
|
||||||
|
<sys:String x:Key="Contrast">Contrasto</sys:String>
|
||||||
|
<sys:String x:Key="InputSource">Sorgente di Ingresso</sys:String>
|
||||||
|
<sys:String x:Key="Power">Alimentazione</sys:String>
|
||||||
|
|
||||||
|
<!-- Power States -->
|
||||||
|
<sys:String x:Key="PowerOn">Accendi</sys:String>
|
||||||
|
<sys:String x:Key="Sleep">Sospendi</sys:String>
|
||||||
|
<sys:String x:Key="PowerOff">Spegni</sys:String>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<sys:String x:Key="Minimize">Riduci a icona</sys:String>
|
||||||
|
<sys:String x:Key="Exit">Esci</sys:String>
|
||||||
|
<sys:String x:Key="Loading">Caricamento...</sys:String>
|
||||||
|
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<sys:String x:Key="LoadConfigErr">Impossibile caricare la configurazione: </sys:String>
|
||||||
|
<sys:String x:Key="SaveConfigErr">Impossibile salvare la configurazione: </sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
29
Language/StringResources.ja.xaml
Normal file
29
Language/StringResources.ja.xaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
|
<!-- Application -->
|
||||||
|
<sys:String x:Key="AppTitle">モニター制御管理</sys:String>
|
||||||
|
<sys:String x:Key="ScreenSwitch">画面スイッチ</sys:String>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<sys:String x:Key="Brightness">輝度</sys:String>
|
||||||
|
<sys:String x:Key="Contrast">コントラスト</sys:String>
|
||||||
|
<sys:String x:Key="InputSource">入力ソース</sys:String>
|
||||||
|
<sys:String x:Key="Power">電源</sys:String>
|
||||||
|
|
||||||
|
<!-- Power States -->
|
||||||
|
<sys:String x:Key="PowerOn">電源オン</sys:String>
|
||||||
|
<sys:String x:Key="Sleep">スリープ</sys:String>
|
||||||
|
<sys:String x:Key="PowerOff">電源オフ</sys:String>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<sys:String x:Key="Minimize">最小化</sys:String>
|
||||||
|
<sys:String x:Key="Exit">終了</sys:String>
|
||||||
|
<sys:String x:Key="Loading">読み込み中...</sys:String>
|
||||||
|
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<sys:String x:Key="LoadConfigErr">設定の読み込みに失敗しました: </sys:String>
|
||||||
|
<sys:String x:Key="SaveConfigErr">設定の保存に失敗しました: </sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
29
Language/StringResources.pt.xaml
Normal file
29
Language/StringResources.pt.xaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
|
<!-- Application -->
|
||||||
|
<sys:String x:Key="AppTitle">Controle de Monitor</sys:String>
|
||||||
|
<sys:String x:Key="ScreenSwitch">Interruptor de Tela</sys:String>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<sys:String x:Key="Brightness">Brilho</sys:String>
|
||||||
|
<sys:String x:Key="Contrast">Contraste</sys:String>
|
||||||
|
<sys:String x:Key="InputSource">Fonte de Entrada</sys:String>
|
||||||
|
<sys:String x:Key="Power">Energia</sys:String>
|
||||||
|
|
||||||
|
<!-- Power States -->
|
||||||
|
<sys:String x:Key="PowerOn">Ligar</sys:String>
|
||||||
|
<sys:String x:Key="Sleep">Suspender</sys:String>
|
||||||
|
<sys:String x:Key="PowerOff">Desligar</sys:String>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<sys:String x:Key="Minimize">Minimizar</sys:String>
|
||||||
|
<sys:String x:Key="Exit">Sair</sys:String>
|
||||||
|
<sys:String x:Key="Loading">Carregando...</sys:String>
|
||||||
|
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<sys:String x:Key="LoadConfigErr">Falha ao carregar a configuração: </sys:String>
|
||||||
|
<sys:String x:Key="SaveConfigErr">Falha ao salvar a configuração: </sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
@ -1,5 +1,29 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
</ResourceDictionary>
|
<!-- Application (Default: English) -->
|
||||||
|
<sys:String x:Key="AppTitle">Monitor Control Management</sys:String>
|
||||||
|
<sys:String x:Key="ScreenSwitch">Screen Switch</sys:String>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<sys:String x:Key="Brightness">Brightness</sys:String>
|
||||||
|
<sys:String x:Key="Contrast">Contrast</sys:String>
|
||||||
|
<sys:String x:Key="InputSource">Input Source</sys:String>
|
||||||
|
<sys:String x:Key="Power">Power</sys:String>
|
||||||
|
|
||||||
|
<!-- Power States -->
|
||||||
|
<sys:String x:Key="PowerOn">Power On</sys:String>
|
||||||
|
<sys:String x:Key="Sleep">Sleep</sys:String>
|
||||||
|
<sys:String x:Key="PowerOff">Power Off</sys:String>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<sys:String x:Key="Minimize">Minimize</sys:String>
|
||||||
|
<sys:String x:Key="Exit">Exit</sys:String>
|
||||||
|
<sys:String x:Key="Loading">Loading...</sys:String>
|
||||||
|
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<sys:String x:Key="LoadConfigErr">Failed to load configuration: </sys:String>
|
||||||
|
<sys:String x:Key="SaveConfigErr">Failed to save configuration: </sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
|
|||||||
29
Language/StringResources.zh.xaml
Normal file
29
Language/StringResources.zh.xaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
|
<!-- Application -->
|
||||||
|
<sys:String x:Key="AppTitle">螢幕控制管理</sys:String>
|
||||||
|
<sys:String x:Key="ScreenSwitch">螢幕開關</sys:String>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<sys:String x:Key="Brightness">亮度</sys:String>
|
||||||
|
<sys:String x:Key="Contrast">對比度</sys:String>
|
||||||
|
<sys:String x:Key="InputSource">輸入源</sys:String>
|
||||||
|
<sys:String x:Key="Power">電源</sys:String>
|
||||||
|
|
||||||
|
<!-- Power States -->
|
||||||
|
<sys:String x:Key="PowerOn">開啟</sys:String>
|
||||||
|
<sys:String x:Key="Sleep">休眠</sys:String>
|
||||||
|
<sys:String x:Key="PowerOff">關閉</sys:String>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<sys:String x:Key="Minimize">最小化</sys:String>
|
||||||
|
<sys:String x:Key="Exit">結束</sys:String>
|
||||||
|
<sys:String x:Key="Loading">載入中...</sys:String>
|
||||||
|
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<sys:String x:Key="LoadConfigErr">載入設定檔失敗: </sys:String>
|
||||||
|
<sys:String x:Key="SaveConfigErr">保存設定檔失敗: </sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RootNamespace>CMM.Library</RootNamespace>
|
<RootNamespace>CMM.Library</RootNamespace>
|
||||||
<AssemblyName>CMM.Library</AssemblyName>
|
<AssemblyName>CMM.Library</AssemblyName>
|
||||||
|
|||||||
@ -30,13 +30,13 @@ public static class CMMCommand
|
|||||||
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} D6 4");
|
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} D6 4");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string> GetMonitorValue(string monitorSN, int? reTry = 0)
|
private static async Task<string> GetMonitorValue(string monitorSN, string vcpCode = "D6", int? reTry = 0)
|
||||||
{
|
{
|
||||||
var value = string.Empty;
|
var value = string.Empty;
|
||||||
while (reTry <= 5)
|
while (reTry <= 5)
|
||||||
{
|
{
|
||||||
var cmdFileName = Path.Combine(CMMTmpFolder, $"{Guid.NewGuid()}.bat");
|
var cmdFileName = Path.Combine(CMMTmpFolder, $"{Guid.NewGuid()}.bat");
|
||||||
var cmd = $"{CMMexe} /GetValue {monitorSN} D6\r\n" +
|
var cmd = $"{CMMexe} /GetValue {monitorSN} {vcpCode}\r\n" +
|
||||||
$"echo %errorlevel%";
|
$"echo %errorlevel%";
|
||||||
File.WriteAllText(cmdFileName, cmd);
|
File.WriteAllText(cmdFileName, cmd);
|
||||||
var values = await ConsoleHelper.ExecuteCommand(cmdFileName);
|
var values = await ConsoleHelper.ExecuteCommand(cmdFileName);
|
||||||
@ -46,12 +46,112 @@ public static class CMMCommand
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(value) && value != "0") return value;
|
if (!string.IsNullOrEmpty(value) && value != "0") return value;
|
||||||
await Task.Delay(500);
|
await Task.Delay(500);
|
||||||
await GetMonitorValue(monitorSN, reTry++);
|
await GetMonitorValue(monitorSN, vcpCode, reTry++);
|
||||||
};
|
};
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Brightness (VCP Code 10)
|
||||||
|
|
||||||
|
public static Task SetBrightness(string monitorSN, int value)
|
||||||
|
{
|
||||||
|
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 10 {value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<int?> GetBrightness(string monitorSN)
|
||||||
|
{
|
||||||
|
var value = await GetMonitorValue(monitorSN, "10");
|
||||||
|
return int.TryParse(value, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Contrast (VCP Code 12)
|
||||||
|
|
||||||
|
public static Task SetContrast(string monitorSN, int value)
|
||||||
|
{
|
||||||
|
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 12 {value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<int?> GetContrast(string monitorSN)
|
||||||
|
{
|
||||||
|
var value = await GetMonitorValue(monitorSN, "12");
|
||||||
|
return int.TryParse(value, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Input Source (VCP Code 60)
|
||||||
|
|
||||||
|
public static Task SetInputSource(string monitorSN, int value)
|
||||||
|
{
|
||||||
|
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 60 {value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<int?> GetInputSource(string monitorSN)
|
||||||
|
{
|
||||||
|
var value = await GetMonitorValue(monitorSN, "60");
|
||||||
|
return int.TryParse(value, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<List<InputSourceOption>> GetInputSourceOptions(string monitorSN)
|
||||||
|
{
|
||||||
|
var options = new List<InputSourceOption>();
|
||||||
|
var savePath = Path.Combine(CMMTmpFolder, $"{monitorSN}_vcp.tmp");
|
||||||
|
|
||||||
|
await ConsoleHelper.CmdCommandAsync($"{CMMexe} /sjson {savePath} {monitorSN}");
|
||||||
|
|
||||||
|
if (!File.Exists(savePath)) return options;
|
||||||
|
|
||||||
|
var monitorModel = JsonHelper.JsonFormFile<IEnumerable<SMonitorModel>>(savePath);
|
||||||
|
var inputSourceVcp = monitorModel?.FirstOrDefault(m => m.VCPCode == "60");
|
||||||
|
|
||||||
|
if (inputSourceVcp?.PossibleValues != null)
|
||||||
|
{
|
||||||
|
var possibleValues = inputSourceVcp.PossibleValues
|
||||||
|
.Split(",", StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(v => int.TryParse(v.Trim(), out var val) ? val : (int?)null)
|
||||||
|
.Where(v => v.HasValue)
|
||||||
|
.Select(v => v.Value);
|
||||||
|
|
||||||
|
foreach (var value in possibleValues)
|
||||||
|
{
|
||||||
|
options.Add(new InputSourceOption(value, GetInputSourceName(value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetInputSourceName(int vcpValue)
|
||||||
|
{
|
||||||
|
return vcpValue switch
|
||||||
|
{
|
||||||
|
1 => "VGA-1",
|
||||||
|
2 => "VGA-2",
|
||||||
|
3 => "DVI-1",
|
||||||
|
4 => "DVI-2",
|
||||||
|
5 => "Composite-1",
|
||||||
|
6 => "Composite-2",
|
||||||
|
7 => "S-Video-1",
|
||||||
|
8 => "S-Video-2",
|
||||||
|
9 => "Tuner-1",
|
||||||
|
10 => "Tuner-2",
|
||||||
|
11 => "Tuner-3",
|
||||||
|
12 => "Component-1",
|
||||||
|
13 => "Component-2",
|
||||||
|
14 => "Component-3",
|
||||||
|
15 => "DisplayPort-1",
|
||||||
|
16 => "DisplayPort-2",
|
||||||
|
17 => "HDMI-1",
|
||||||
|
18 => "HDMI-2",
|
||||||
|
_ => $"Input-{vcpValue}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public static async Task<string> GetMonPowerStatus(string monitorSN)
|
public static async Task<string> GetMonPowerStatus(string monitorSN)
|
||||||
{
|
{
|
||||||
var status = await GetMonitorValue(monitorSN);
|
var status = await GetMonitorValue(monitorSN);
|
||||||
@ -120,6 +220,9 @@ public static class CMMCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly string DebugLog = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "MonitorParse.log");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 取得螢幕清單
|
/// 取得螢幕清單
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -129,55 +232,54 @@ public static class CMMCommand
|
|||||||
|
|
||||||
if (!File.Exists(CMMsMonitors)) return monitors;
|
if (!File.Exists(CMMsMonitors)) return monitors;
|
||||||
|
|
||||||
XMonitor mon = null;
|
// Try reading raw bytes to understand encoding
|
||||||
string context;
|
var rawBytes = await File.ReadAllBytesAsync(CMMsMonitors);
|
||||||
foreach (var line in await File.ReadAllLinesAsync(CMMsMonitors))
|
File.WriteAllText(DebugLog, $"File size: {rawBytes.Length} bytes\nFirst 20 bytes: {BitConverter.ToString(rawBytes.Take(20).ToArray())}\n\n");
|
||||||
|
|
||||||
|
// Try UTF-16 LE (common Windows Unicode)
|
||||||
|
var content = System.Text.Encoding.Unicode.GetString(rawBytes);
|
||||||
|
File.AppendAllText(DebugLog, $"Content preview:\n{content.Substring(0, Math.Min(500, content.Length))}\n\n");
|
||||||
|
|
||||||
|
XMonitor? mon = null;
|
||||||
|
foreach (var line in content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
|
||||||
{
|
{
|
||||||
var sp = line.Split(":", StringSplitOptions.RemoveEmptyEntries);
|
File.AppendAllText(DebugLog, $"LINE: [{line}]\n");
|
||||||
try
|
|
||||||
{
|
|
||||||
if (sp.Length != 2 || string.IsNullOrEmpty(sp[1])) continue;
|
|
||||||
|
|
||||||
context = sp[1].Substring(2, sp[1].Length - 3);
|
var colonIdx = line.IndexOf(':');
|
||||||
}
|
if (colonIdx < 0) continue;
|
||||||
catch
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sp[0].StartsWith("Monitor Device Name"))
|
var key = line.Substring(0, colonIdx).Trim();
|
||||||
{
|
var val = line.Substring(colonIdx + 1).Trim().Trim('"');
|
||||||
mon = new XMonitor();
|
|
||||||
mon.MonitorDeviceName = context;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sp[0].StartsWith("Monitor Name"))
|
File.AppendAllText(DebugLog, $" KEY=[{key}] VAL=[{val}]\n");
|
||||||
{
|
|
||||||
mon.MonitorName = context;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sp[0].StartsWith("Serial Number"))
|
if (key.Contains("Monitor Device Name"))
|
||||||
{
|
{
|
||||||
mon.SerialNumber = context;
|
mon = new XMonitor { MonitorDeviceName = val };
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
else if (mon != null && key.Contains("Monitor Name"))
|
||||||
if (sp[0].StartsWith("Adapter Name"))
|
|
||||||
{
|
{
|
||||||
mon.AdapterName = context;
|
mon.MonitorName = val;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
else if (mon != null && key.Contains("Serial Number"))
|
||||||
if (sp[0].StartsWith("Monitor ID"))
|
|
||||||
{
|
{
|
||||||
mon.MonitorID = context;
|
mon.SerialNumber = val;
|
||||||
monitors.Add(mon);
|
}
|
||||||
continue;
|
else if (mon != null && key.Contains("Adapter Name"))
|
||||||
|
{
|
||||||
|
mon.AdapterName = val;
|
||||||
|
}
|
||||||
|
else if (mon != null && key.Contains("Monitor ID"))
|
||||||
|
{
|
||||||
|
mon.MonitorID = val;
|
||||||
|
File.AppendAllText(DebugLog, $" -> Adding monitor: Name=[{mon.MonitorName}] SN=[{mon.SerialNumber}]\n");
|
||||||
|
if (!string.IsNullOrEmpty(mon.SerialNumber))
|
||||||
|
monitors.Add(mon);
|
||||||
|
mon = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File.AppendAllText(DebugLog, $"\nTotal monitors with serial: {monitors.Count}\n");
|
||||||
return monitors;
|
return monitors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
namespace CMM.Library.ViewModel
|
namespace CMM.Library.ViewModel
|
||||||
{
|
{
|
||||||
|
public record InputSourceOption(int Value, string Name)
|
||||||
|
{
|
||||||
|
public override string ToString() => Name;
|
||||||
|
}
|
||||||
|
|
||||||
public class XMonitorStatus : PropertyBase
|
public class XMonitorStatus : PropertyBase
|
||||||
{
|
{
|
||||||
public string VCP_Code
|
public string VCP_Code
|
||||||
|
|||||||
74
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# ControlMyMonitorManagement
|
||||||
|
|
||||||
|
A Windows desktop application for controlling monitor settings via DDC/CI (Display Data Channel/Command Interface). Adjust brightness, contrast, power state, and other display parameters across multiple monitors.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multi-Monitor Support** - Detect and control multiple connected displays
|
||||||
|
- **Brightness & Contrast Sliders** - Easy adjustment with real-time feedback
|
||||||
|
- **Input Source Switching** - Switch between VGA, DVI, HDMI, DisplayPort (auto-detected from monitor)
|
||||||
|
- **Power Management** - Turn monitors on, off, or into sleep mode
|
||||||
|
- **System Tray Integration** - DellMonitorControl app with taskbar tray popup
|
||||||
|
- **9 Languages** - Auto-detects system language (en, es, fr, de, zh, ja, pt, it, hi)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Windows OS
|
||||||
|
- .NET 7.0 SDK
|
||||||
|
- DDC/CI compatible monitor(s)
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/yourusername/ControlMyMonitorManagement.git
|
||||||
|
cd ControlMyMonitorManagement
|
||||||
|
|
||||||
|
# Build the solution
|
||||||
|
dotnet build ControlMyMonitorManagement.sln
|
||||||
|
|
||||||
|
# Run the main application
|
||||||
|
dotnet run --project ControlMyMonitorManagement/ControlMyMonitorManagement.csproj
|
||||||
|
|
||||||
|
# Run the Dell-specific application (with system tray)
|
||||||
|
dotnet run --project DellMonitorControl/DellMonitorControl.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
| Project | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| **ControlMyMonitorManagement** | Main WPF application with full UI |
|
||||||
|
| **DellMonitorControl** | Lightweight app with system tray integration |
|
||||||
|
| **Library** | Core business logic and DDC/CI operations |
|
||||||
|
| **CMMModel** | Data models for monitor status |
|
||||||
|
| **CMMService** | Service layer interfaces |
|
||||||
|
| **Language** | Localization support |
|
||||||
|
| **Tester** | NUnit test project |
|
||||||
|
|
||||||
|
## Common VCP Codes
|
||||||
|
|
||||||
|
| Code | Name | Values |
|
||||||
|
|------|------|--------|
|
||||||
|
| D6 | Power Mode | 1=On, 4=Sleep, 5=Off |
|
||||||
|
| 10 | Brightness | 0-100 |
|
||||||
|
| 12 | Contrast | 0-100 |
|
||||||
|
| 60 | Input Source | 1=VGA, 3=DVI, 15=DisplayPort, 17=HDMI |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
This application uses DDC/CI protocol to communicate with monitors through the display cable (HDMI, DisplayPort, DVI, VGA). DDC/CI allows software to send commands directly to the monitor's firmware to adjust settings that would normally require using the monitor's physical buttons.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- **Original Project**: [DangWang](https://github.com/poyingHAHA) - ControlMyMonitorManagement
|
||||||
|
- **DDC/CI Tool**: [Nir Sofer](https://www.nirsoft.net) - ControlMyMonitor.exe (freeware)
|
||||||
|
- **Enhancements by David H Friedel Jr**: Brightness/contrast sliders, input source switching, 9-language localization
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project contains multiple components with different terms:
|
||||||
|
|
||||||
|
- **Original code by DangWang**: No explicit license provided
|
||||||
|
- **ControlMyMonitor.exe**: Freeware by Nir Sofer - free distribution allowed, no commercial use, no modification
|
||||||
|
- **New contributions (sliders, localization, etc.)**: MIT License
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RootNamespace>CMM.Tester</RootNamespace>
|
<RootNamespace>CMM.Tester</RootNamespace>
|
||||||
<Product>ControlMyMonitorManagement</Product>
|
<Product>ControlMyMonitorManagement</Product>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user