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">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<AssemblyName>CMM.Model</AssemblyName>
|
||||
<RootNamespace>CMM.Model</RootNamespace>
|
||||
<Product>ControlMyMonitorManagement</Product>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AssemblyName>CMM.Service</AssemblyName>
|
||||
<RootNamespace>CMM.Service</RootNamespace>
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
using CMM.Library.Config;
|
||||
using CMM.Language;
|
||||
using CMM.Library.Config;
|
||||
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.Windows;
|
||||
|
||||
@ -17,6 +13,7 @@ namespace CMM.Management
|
||||
{
|
||||
internal static XConfig cfg { get; private set; }
|
||||
internal static CMMMgr CMMMgr { get; private set; }
|
||||
private readonly CulturesHelper _culturesHelper = new();
|
||||
|
||||
public App()
|
||||
{
|
||||
@ -28,6 +25,7 @@ namespace CMM.Management
|
||||
|
||||
private async Task App_Startup(object sender, StartupEventArgs e)
|
||||
{
|
||||
_culturesHelper.ApplySystemLanguage();
|
||||
await CMMMgr.Init();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows;
|
||||
using CMM.Library.ViewModel;
|
||||
using CMM.Library.Base;
|
||||
using CMM.Library.Method;
|
||||
using System.Windows.Data;
|
||||
using CMM.Language;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CMM.Management.Control
|
||||
{
|
||||
/// <summary>
|
||||
/// 單一顆螢幕
|
||||
/// Single monitor control
|
||||
/// </summary>
|
||||
internal class MonCtrl : System.Windows.Controls.Control
|
||||
{
|
||||
public readonly static DependencyProperty MonProperty;
|
||||
public static readonly DependencyProperty MonProperty;
|
||||
private StackPanel _sp;
|
||||
|
||||
static MonCtrl()
|
||||
@ -30,6 +31,10 @@ namespace CMM.Management.Control
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
_sp = Template.FindName("sp", this) as StackPanel;
|
||||
if (_sp != null && Mon != null)
|
||||
{
|
||||
_ = LoadControlsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public XMonitor Mon
|
||||
@ -49,7 +54,236 @@ namespace CMM.Management.Control
|
||||
|
||||
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>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<RootNamespace>CMM.Management</RootNamespace>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Copyright>Copyright © DangWang $([System.DateTime]::Now.ToString(yyyy))</Copyright>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
xmlns:ctrl="clr-namespace:CMM.Management.Control"
|
||||
xmlns:local="clr-namespace:CMM.Management"
|
||||
mc:Ignorable="d"
|
||||
Title="ControlMyMonitor Management"
|
||||
Title="{DynamicResource AppTitle}"
|
||||
Background="#666C6161"
|
||||
AllowsTransparency="True"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
|
||||
@ -1,15 +1,30 @@
|
||||
<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:local="clr-namespace:CMM.Management.Control">
|
||||
|
||||
<Style TargetType="{x:Type local:MonCtrl}">
|
||||
<Setter Property="Margin" Value="10"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type local:MonCtrl}">
|
||||
<StackPanel x:Name="sp" Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<TextBlock Text="{Binding Mon.MonitorName, Mode=OneWay, NotifyOnSourceUpdated=True, NotifyOnValidationError=True,
|
||||
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MonCtrl}}}"/>
|
||||
<Border Background="#44000000" CornerRadius="8" Padding="15">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<!-- Monitor Name Header -->
|
||||
<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>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
<Application x:Class="DellMonitorControl.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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"
|
||||
StartupUri="MainWindow.xaml">
|
||||
Startup="Application_Startup">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/Style/Color.xaml"/>
|
||||
<ResourceDictionary Source="/Images/Icons.xaml"/>
|
||||
<ResourceDictionary Source="/Style/Main.xaml"/>
|
||||
<ResourceDictionary Source="/Style/Btn.xaml"/>
|
||||
<ResourceDictionary Source="/Style/Slider.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
<tb:TaskbarIcon x:Key="TrayIcon"
|
||||
IconSource="/DellLogo.ico"
|
||||
ToolTipText="Monitor Control"/>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@ -1,17 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using System.Windows;
|
||||
|
||||
namespace DellMonitorControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
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"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
mc:Ignorable="d"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:DellMonitorControl"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Border Background="{DynamicResource H3}" CornerRadius="10" MouseLeave="Border_MouseLeave"/>
|
||||
Width="280" Height="300">
|
||||
<Grid Background="#333333">
|
||||
<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>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
using CMM.Library.Method;
|
||||
using CMM.Language;
|
||||
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.Controls;
|
||||
@ -19,58 +23,433 @@ public partial class ControlPanel : UserControl
|
||||
|
||||
public async Task Refresh()
|
||||
{
|
||||
await CMMCommand.ScanMonitor();
|
||||
var monitors = await CMMCommand.ReadMonitorsData();
|
||||
try
|
||||
{
|
||||
sp.Children.Clear();
|
||||
|
||||
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 status = await CMMCommand.GetMonPowerStatus(m.SerialNumber);
|
||||
var ctrl = CreatControl(m, status);
|
||||
sp.Children.Add(ctrl);
|
||||
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();
|
||||
|
||||
_sp.Orientation = Orientation.Vertical;
|
||||
_sp.Margin = new Thickness(10, 5, 5, 0);
|
||||
|
||||
var tb = new TextBlock
|
||||
var container = new StackPanel
|
||||
{
|
||||
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,
|
||||
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")
|
||||
};
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Tag = monitorModel.SerialNumber,
|
||||
Content = powerStatus,
|
||||
Style = (Style)FindResource("TextButtonStyle")
|
||||
Tag = monitorSN,
|
||||
Content = GetLocalizedPowerStatus(powerStatus),
|
||||
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);
|
||||
_sp.Children.Add(btn);
|
||||
panel.Children.Add(labelBlock);
|
||||
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 tag = btn?.Tag.ToString();
|
||||
var content = btn?.Content as string;
|
||||
if (content == "Sleep")
|
||||
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);
|
||||
}
|
||||
@ -80,6 +459,34 @@ public partial class ControlPanel : UserControl
|
||||
}
|
||||
|
||||
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>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>DellMonitorControl</AssemblyName>
|
||||
<RootNamespace>DellMonitorControl</RootNamespace>
|
||||
@ -45,6 +45,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Library\Library.csproj" />
|
||||
<ProjectReference Include="..\Language\Language.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,21 +1,15 @@
|
||||
<Window x:Class="DellMonitorControl.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tb="clr-namespace:Hardcodet.Wpf.TaskbarNotification;assembly=Hardcodet.Wpf.TaskbarNotification.Net6"
|
||||
xmlns:local="clr-namespace:DellMonitorControl"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="450" Width="800">
|
||||
Title="Monitor Control" Height="400" Width="320"
|
||||
WindowStyle="ToolWindow"
|
||||
ResizeMode="CanResize"
|
||||
Background="#333333">
|
||||
|
||||
<Grid>
|
||||
<tb:TaskbarIcon x:Name="taskbar"
|
||||
IconSource="/DellLogo.ico"
|
||||
ToolTipText="螢幕開關"
|
||||
PopupActivation="LeftOrDoubleClick">
|
||||
<tb:TaskbarIcon.TrayPopup>
|
||||
<local:ControlPanel x:Name="comtrolPanel"/>
|
||||
</tb:TaskbarIcon.TrayPopup>
|
||||
</tb:TaskbarIcon>
|
||||
</Grid>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Margin="10">
|
||||
<StackPanel Name="sp">
|
||||
<TextBlock Text="Monitor Control" Foreground="White" FontSize="16" FontWeight="Bold"/>
|
||||
<TextBlock Text="Loading..." Foreground="LightGray" FontSize="12" Margin="0,10,0,0"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</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.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace DellMonitorControl;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.Hide();
|
||||
taskbar.TrayPopupOpen += async (s, e) => await Taskbar_TrayPopupOpen(s, e);
|
||||
PositionWindowNearTray();
|
||||
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>();
|
||||
string uriPath = ResourceLocalPath + "/" + LanguageFileName;
|
||||
|
||||
Languages.Add(uriPath + ".en-US.xaml");
|
||||
Languages.Add(uriPath + ".zh-TW.xaml");
|
||||
Languages.Add(uriPath + ".en.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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var cultureInfo = CultureInfo.GetCultureInfo(cultureName);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<RootNamespace>CMM.Language</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
@ -22,13 +22,34 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="StringResources.en-US.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</None>
|
||||
<None Update="StringResources.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</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>
|
||||
</None>
|
||||
</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:sys="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>CMM.Library</RootNamespace>
|
||||
<AssemblyName>CMM.Library</AssemblyName>
|
||||
|
||||
@ -30,13 +30,13 @@ public static class CMMCommand
|
||||
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;
|
||||
while (reTry <= 5)
|
||||
{
|
||||
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%";
|
||||
File.WriteAllText(cmdFileName, cmd);
|
||||
var values = await ConsoleHelper.ExecuteCommand(cmdFileName);
|
||||
@ -46,12 +46,112 @@ public static class CMMCommand
|
||||
|
||||
if (!string.IsNullOrEmpty(value) && value != "0") return value;
|
||||
await Task.Delay(500);
|
||||
await GetMonitorValue(monitorSN, reTry++);
|
||||
await GetMonitorValue(monitorSN, vcpCode, reTry++);
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
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>
|
||||
@ -129,55 +232,54 @@ public static class CMMCommand
|
||||
|
||||
if (!File.Exists(CMMsMonitors)) return monitors;
|
||||
|
||||
XMonitor mon = null;
|
||||
string context;
|
||||
foreach (var line in await File.ReadAllLinesAsync(CMMsMonitors))
|
||||
{
|
||||
var sp = line.Split(":", StringSplitOptions.RemoveEmptyEntries);
|
||||
try
|
||||
{
|
||||
if (sp.Length != 2 || string.IsNullOrEmpty(sp[1])) continue;
|
||||
// Try reading raw bytes to understand encoding
|
||||
var rawBytes = await File.ReadAllBytesAsync(CMMsMonitors);
|
||||
File.WriteAllText(DebugLog, $"File size: {rawBytes.Length} bytes\nFirst 20 bytes: {BitConverter.ToString(rawBytes.Take(20).ToArray())}\n\n");
|
||||
|
||||
context = sp[1].Substring(2, sp[1].Length - 3);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 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");
|
||||
|
||||
if (sp[0].StartsWith("Monitor Device Name"))
|
||||
XMonitor? mon = null;
|
||||
foreach (var line in content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
mon = new XMonitor();
|
||||
mon.MonitorDeviceName = context;
|
||||
continue;
|
||||
}
|
||||
File.AppendAllText(DebugLog, $"LINE: [{line}]\n");
|
||||
|
||||
if (sp[0].StartsWith("Monitor Name"))
|
||||
{
|
||||
mon.MonitorName = context;
|
||||
continue;
|
||||
}
|
||||
var colonIdx = line.IndexOf(':');
|
||||
if (colonIdx < 0) continue;
|
||||
|
||||
if (sp[0].StartsWith("Serial Number"))
|
||||
{
|
||||
mon.SerialNumber = context;
|
||||
continue;
|
||||
}
|
||||
var key = line.Substring(0, colonIdx).Trim();
|
||||
var val = line.Substring(colonIdx + 1).Trim().Trim('"');
|
||||
|
||||
if (sp[0].StartsWith("Adapter Name"))
|
||||
{
|
||||
mon.AdapterName = context;
|
||||
continue;
|
||||
}
|
||||
File.AppendAllText(DebugLog, $" KEY=[{key}] VAL=[{val}]\n");
|
||||
|
||||
if (sp[0].StartsWith("Monitor ID"))
|
||||
if (key.Contains("Monitor Device Name"))
|
||||
{
|
||||
mon.MonitorID = context;
|
||||
mon = new XMonitor { MonitorDeviceName = val };
|
||||
}
|
||||
else if (mon != null && key.Contains("Monitor Name"))
|
||||
{
|
||||
mon.MonitorName = val;
|
||||
}
|
||||
else if (mon != null && key.Contains("Serial Number"))
|
||||
{
|
||||
mon.SerialNumber = val;
|
||||
}
|
||||
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);
|
||||
continue;
|
||||
mon = null;
|
||||
}
|
||||
}
|
||||
|
||||
File.AppendAllText(DebugLog, $"\nTotal monitors with serial: {monitors.Count}\n");
|
||||
return monitors;
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,11 @@
|
||||
|
||||
namespace CMM.Library.ViewModel
|
||||
{
|
||||
public record InputSourceOption(int Value, string Name)
|
||||
{
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public class XMonitorStatus : PropertyBase
|
||||
{
|
||||
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">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>CMM.Tester</RootNamespace>
|
||||
<Product>ControlMyMonitorManagement</Product>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user