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:
David H. Friedel Jr. 2026-01-03 22:11:35 -05:00
parent 6b656ead2d
commit 0352c6b755
34 changed files with 1640 additions and 158 deletions

View 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
View 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

View File

@ -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>

View File

@ -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>

View File

@ -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();
} }
} }

View File

@ -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);
} }
} }
} }

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
} }
} }

View File

@ -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>

View File

@ -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);
} }
} }

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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.

View File

@ -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);

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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;
} }

View File

@ -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
View 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

View File

@ -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>