Skip to content

Instantly share code, notes, and snippets.

@yuru4c
Created June 19, 2018 10:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yuru4c/2d9b5aafad5b707072ae57caefcf73c7 to your computer and use it in GitHub Desktop.
Save yuru4c/2d9b5aafad5b707072ae57caefcf73c7 to your computer and use it in GitHub Desktop.
f.lux を制御します
## FluxController.csproj
### f.lux (v4.66) を制御します
**Classic f.lux と Use wider slider ranges を有効にすること。**
引数に色温度または `auto` を指定できます。
`auto` の場合はカメラから色温度を取得します。
後ろに `-quiet` オプションを付けるとエラーを表示しません。
タスクスケジューラから適宜実行することで環境光と画面の色温度を合わせられます。
---
- カメラ選択は Camera.cs の 32 行目
- Designer.cs を手動で編集したためデザイナーで開くことができない
- UI の挙動が怪しい 非同期だと更新がうまく行かない
- 最新版で動くかは知らない
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.Devices.Enumeration;
using Windows.Media.Capture;
using Windows.Media.Devices;
using Windows.Media.MediaProperties;
using Windows.Storage.Streams;
using Timer = System.Timers.Timer;
namespace FluxController
{
internal static class Camera
{
internal delegate void Callback(uint temperature);
private static bool _inited;
private static MediaCapture _capture;
private static MediaEncodingProfile _profile;
private static bool _measuring;
private static Task _task;
private static CancellationTokenSource _source;
private static uint Temperature =>
_capture.VideoDeviceController.WhiteBalanceControl.Value;
private static async Task Init()
{
var cameras = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
var camera =
cameras.FirstOrDefault(item => item.EnclosureLocation?.Panel == Panel.Front)
?? cameras.First();
_capture = new MediaCapture();
await _capture.InitializeAsync(new MediaCaptureInitializationSettings
{
VideoDeviceId = camera.Id
});
await _capture
.VideoDeviceController
.WhiteBalanceControl
.SetPresetAsync(ColorTemperaturePreset.Auto);
_profile = MediaEncodingProfile.CreateAvi(VideoEncodingQuality.Qvga);
_profile.Video.FrameRate.Numerator = 1;
_profile.Video.FrameRate.Denominator = uint.MaxValue;
}
internal static async Task Stop()
{
if (_measuring)
{
_measuring = false;
_source.Cancel();
await _capture.StopRecordAsync();
if (_task != null) try { await _task; } catch (TaskCanceledException) { }
}
}
internal static async Task GetTemperature(Callback callback, int interval = 0)
{
if (_inited) await Stop();
else
{
await Init();
_inited = true;
}
_measuring = true;
_source = new CancellationTokenSource();
using (var stream = new InMemoryRandomAccessStream())
{
await _capture.StartRecordToStreamAsync(_profile, stream);
await Task.Delay(Helper.Slow);
if (interval == 0)
{
callback(Temperature);
return;
}
using (var timer = new Timer(interval))
{
timer.Elapsed += (s, a) => callback(Temperature);
timer.Start();
_task = Task.Delay(Timeout.Infinite, _source.Token);
try { await _task; } catch (TaskCanceledException) { }
}
}
}
}
}
using System;
using System.ComponentModel;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static FluxController.NativeMethods;
namespace FluxController
{
internal static class Controller
{
private const ushort OutY = 34, InY = 60; // , BedY = 87;
private const double FrameRate = 1000D / 60D;
private const double MinSpan = 5E-5;
private const double Half = Math.PI / 2;
private static readonly IntPtr OpenParam = MAKELPARAM(347, 229);
private static readonly IntPtr CloseParam = MAKELPARAM( 44, 36);
private static IntPtr _hwndFlux;
private static bool _opened;
private static bool _animating;
private static Task _task;
private static CancellationTokenSource _source;
private static bool IsNull => _hwndFlux == nullptr;
private static bool EnumFunc(IntPtr hWnd, IntPtr lParam)
{
var name = new StringBuilder(256);
GetClassName(hWnd, name, name.Capacity);
if (name.ToString() != "ytWindow") return true;
var text = new StringBuilder(GetWindowTextLength(hWnd) + 1);
GetWindowText(hWnd, text, text.Capacity);
if (!text.ToString().StartsWith("f.lux: ")) return true;
_hwndFlux = hWnd;
return false;
}
private static void Click(IntPtr lParam)
{
PostMessage(_hwndFlux, WM_LBUTTONDOWN, nullptr, lParam);
PostMessage(_hwndFlux, WM_LBUTTONUP, nullptr, lParam);
}
private static void Click(ushort x, ushort y)
{
Click(MAKELPARAM(x, y));
}
internal static void Open()
{
if (_opened) return;
if (IsNull)
{
EnumWindows(EnumFunc, nullptr);
if (IsNull) throw new Win32Exception();
}
Click(OpenParam);
_opened = true;
}
internal static void Close()
{
if (!_opened) return;
Click(CloseParam);
_opened = false;
}
private static void Set(uint temperature, bool up)
{
// (1 - (1/t - 1/9300) / (1/1200 - 1/9300)) * (543 - 152) + 152;
var x = (ushort)((temperature * 13U + 25710U) / 270U);
if (up) { Click(x, InY); Click(x, OutY); }
else { Click(x, OutY); Click(x, InY); }
}
internal static void Set(uint temperature)
{
var up = temperature > Helper.GetCurrentTemperature();
Set(Helper.Limit(temperature), up);
}
internal static async Task Stop()
{
if (_animating)
{
_source.Cancel();
try { await _task; }
catch (TaskCanceledException) { }
}
}
internal static async Task Animate(uint temperature, bool slow)
{
await Stop();
_animating = true;
_source = new CancellationTokenSource();
var current = Helper.GetCurrentTemperature();
var up = temperature > current;
temperature = Helper.Limit(temperature);
var from = 1D / current;
var span = 1D / temperature - from;
var interval = slow ? Helper.Slow : Helper.Fast;
var abs = Math.Abs(span);
if (abs < MinSpan) interval = (int)(interval * (abs / MinSpan));
try
{
using (var timer = new System.Timers.Timer(FrameRate))
{
var now = DateTime.Now;
timer.Elapsed += (sender, args) =>
{
var elapsed = (DateTime.Now - now).TotalMilliseconds;
var t = Math.Sin(Half * (elapsed / interval));
Set((uint)(1D / (from + span * (slow ? t * t : t))), up);
};
timer.Start();
_task = Task.Delay(interval, _source.Token);
await _task;
}
Set(temperature, up);
}
finally { _animating = false; }
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>FluxController</RootNamespace>
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
<TargetPlatformVersion>8.1</TargetPlatformVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<UseVSHostingProcess>false</UseVSHostingProcess>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<NoWin32Manifest>true</NoWin32Manifest>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Drawing" />
<Reference Include="System.Runtime.WindowsRuntime">
<HintPath>$(ProgramFiles)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5.1\System.Runtime.WindowsRuntime.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="Windows" />
</ItemGroup>
<ItemGroup>
<Compile Include="Helper.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Camera.cs" />
<Compile Include="Controller.cs" />
<Compile Include="Form1.cs" />
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
<Compile Include="NativeMethods.cs" />
<Compile Include="Program.cs" />
<Compile Include="UserControl1.cs" />
<Compile Include="UserControl1.Designer.cs">
<DependentUpon>UserControl1.cs</DependentUpon>
</Compile>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace FluxController
{
internal partial class Form1 : Form
{
private static readonly uint[] Temperatures = {1900U, 2700U, 3400U, 4200U, 5000U, 6500U};
private bool _animating, _measuring, _closing;
internal Form1()
{
InitializeComponent();
}
private Task Stop()
{
_measuring = false;
control1.Enabled = false;
control1.Text = "停止中…";
return Camera.Stop();
}
private async void Set(uint temperature)
{
control1.Temperature = temperature;
control1.Enabled = true;
if (_closing) await Stop();
}
private void Callback(uint temperature)
{
if (_measuring)
{
Controller.Set(temperature);
Invoke((Camera.Callback)Set, temperature);
}
}
private async Task Start()
{
await Controller.Stop();
_animating = false;
Controller.Open();
await Camera.GetTemperature(Callback, Helper.Fast);
if (!_animating) Controller.Close();
control1.Text = control1.DefaultText;
if (_closing)
{
Close();
return;
}
control1.Enabled = true;
}
private async void control_CheckedChanged(object sender, EventArgs e)
{
var control = (UserControl1)sender;
if (control.Checked)
{
_animating = true;
if (_measuring) await Stop();
try
{
Controller.Open();
await Controller.Animate(control.Temperature, false);
}
catch (TaskCanceledException) { return; }
Controller.Close();
_animating = false;
if (_closing) Close();
}
}
private async void control1_CheckedChanged(object sender, EventArgs e)
{
if (control1.Checked)
{
_measuring = true;
control1.Enabled = false;
control1.Text = "準備中…";
await Start();
}
}
private void Form1_KeyUp(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Escape) Close();
}
private void Form1_Deactivate(object sender, EventArgs e)
{
Close();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (_animating || _measuring)
{
e.Cancel = true;
_closing = true;
tableLayoutPanel1.Enabled = false;
}
}
}
}
namespace FluxController
{
partial class Form1
{
private static readonly int Length = Temperatures.Length;
private void InitializeComponent()
{
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
for (var i = 0; i < Length; i++)
{
this.controls[i] = new UserControl1();
}
this.control1 = new UserControl1();
this.SuspendLayout();
this.groupBox1.SuspendLayout();
this.tableLayoutPanel1.SuspendLayout();
//
// groupBox1
//
this.groupBox1.Dock = System.Windows.Forms.DockStyle.Fill;
this.groupBox1.Text = "色温度";
this.groupBox1.Controls.Add(this.tableLayoutPanel1);
//
// tableLayoutPanel1
//
this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tableLayoutPanel1.RowCount = 2;
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 60F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 40F));
this.tableLayoutPanel1.ColumnCount = Length;
var width = 100F / Length;
for (var i = 0; i < Length; i++)
{
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, width));
}
for (var i = 0; i < Length; i++)
{
this.tableLayoutPanel1.Controls.Add(this.controls[i], i, 0);
}
this.tableLayoutPanel1.Controls.Add(this.control1, 0, 1);
this.tableLayoutPanel1.SetColumnSpan(this.control1, Length);
//
// controls
//
var checkedChanged = new System.EventHandler(control_CheckedChanged);
for (var i = 0; i < Length; i++)
{
var control = this.controls[i];
control.Temperature = Temperatures[i];
control.CheckedChanged += checkedChanged;
}
//
// control1
//
this.control1.DefaultText = "自動";
this.control1.CheckedChanged += new System.EventHandler(this.control1_CheckedChanged);
//
// Form1
//
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.MaximizeBox = false;
this.ClientSize = new System.Drawing.Size(450, 150);
this.ShowIcon = false;
this.Text = Program.Title;
this.TopMost = true;
this.KeyPreview = true;
this.Font = new System.Drawing.Font(System.Windows.Forms.Control.DefaultFont.FontFamily, 12F, System.Drawing.FontStyle.Bold);
this.Controls.Add(this.groupBox1);
this.KeyUp += new System.Windows.Forms.KeyEventHandler(this.Form1_KeyUp);
this.Deactivate += new System.EventHandler(this.Form1_Deactivate);
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
this.tableLayoutPanel1.ResumeLayout(false);
this.groupBox1.ResumeLayout(false);
this.ResumeLayout(false);
}
private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
private UserControl1[] controls = new UserControl1[Length];
private UserControl1 control1;
}
}
using System;
using Microsoft.Win32;
namespace FluxController
{
internal static class Helper
{
internal const int Slow = 400;
internal const int Fast = 200;
internal static uint Limit(uint temperature) =>
Math.Min(Math.Max(temperature, 1200U), 9300U);
private static int GetDword(string name, int defaultValue)
{
var key = Registry.CurrentUser.OpenSubKey(@"Software\Michael Herf\flux\Preferences");
var value = key?.GetValue(name, defaultValue);
return (int?)value ?? defaultValue;
}
internal const int DefaultTemperature = 6500;
internal static int GetCurrentTemperature() =>
GetDword("Outdoor", DefaultTemperature) +
GetDword("Indoor", DefaultTemperature) >> 1;
}
}
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace FluxController
{
internal static class NativeMethods
{
private const string User32 = "user32.dll";
internal static readonly IntPtr nullptr = IntPtr.Zero;
internal delegate bool WNDENUMPROC(IntPtr hWnd, IntPtr lparam);
[DllImport(User32)]
internal static extern IntPtr EnumWindows(WNDENUMPROC lpEnumFunc, IntPtr lParam);
[DllImport(User32, CharSet = CharSet.Auto)]
internal static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport(User32, CharSet = CharSet.Auto)]
internal static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport(User32, CharSet = CharSet.Auto)]
internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport(User32, SetLastError = true)]
internal static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
internal const uint WM_LBUTTONDOWN = 0x201U;
internal const uint WM_LBUTTONUP = 0x202U;
// internal const uint WM_LBUTTONDBLCLK = 0x203U;
// internal const uint WM_RBUTTONDOWN = 0x204U;
// internal const uint WM_RBUTTONUP = 0x205U;
// internal const uint WM_RBUTTONDBLCLK = 0x206U;
internal static IntPtr MAKELPARAM(ushort wLow, ushort wHigh) => (IntPtr)((wHigh << 16) | wLow);
}
}
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace FluxController
{
internal static class Program
{
internal const string Title = "f.lux Controller";
/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
private static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (args.Length > 0)
{
var arg = args[0];
try
{
TrySet(arg).Wait();
}
catch (Exception exception)
{
if (args.Length <= 1 || args[1] != "-quiet")
MessageBox.Show(
exception.Message,
"エラー - " + Title,
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
return;
}
Application.Run(new Form1());
}
private static async Task Animate(uint? temperature, bool auto)
{
await Controller.Animate(temperature ?? Helper.DefaultTemperature, auto);
Controller.Close();
}
private static async Task TrySet(string arg)
{
uint? temperature = null;
var auto = arg == "auto";
if (!auto) temperature = uint.Parse(arg);
for (var i = 0; ; i++)
{
try
{
Controller.Open();
if (temperature == null)
{
await Camera.GetTemperature(value => temperature = value);
await Task.WhenAll(Camera.Stop(), Animate(temperature, auto));
}
else await Animate(temperature, auto);
break;
}
catch
{
if (i >= 20) throw;
await Task.Delay(Helper.Fast);
}
}
}
}
}
using System.Reflection;
using System.Runtime.InteropServices;
// アセンブリに関する一般情報は以下の属性セットをとおして制御されます。
// アセンブリに関連付けられている情報を変更するには、
// これらの属性値を変更してください。
[assembly: AssemblyTitle(FluxController.Program.Title)]
// [assembly: AssemblyDescription("")]
// [assembly: AssemblyConfiguration("")]
// [assembly: AssemblyCompany("")]
// [assembly: AssemblyProduct("FluxController")]
// [assembly: AssemblyCopyright("Copyright © 2016")]
// [assembly: AssemblyTrademark("")]
// [assembly: AssemblyCulture("")]
// ComVisible を false に設定すると、その型はこのアセンブリ内で COM コンポーネントから
// 参照不可能になります。COM からこのアセンブリ内の型にアクセスする場合は、
// その型の ComVisible 属性を true に設定してください。
[assembly: ComVisible(false)]
// このプロジェクトが COM に公開される場合、次の GUID が typelib の ID になります
// [assembly: Guid("")]
// アセンブリのバージョン情報は次の 4 つの値で構成されています:
//
// メジャー バージョン
// マイナー バージョン
// ビルド番号
// Revision
//
// すべての値を指定するか、下のように '*' を使ってビルドおよびリビジョン番号を
// 既定値にすることができます:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
// [assembly: AssemblyFileVersion("1.0.0.0")]
using System.Windows.Forms;
namespace FluxController
{
internal partial class UserControl1 : RadioButton
{
private string _defaultText;
internal string DefaultText
{
get { return _defaultText; }
set
{
_defaultText = value;
Text = value;
}
}
private uint _temperature;
internal uint Temperature
{
get { return _temperature; }
set
{
_temperature = value;
Text = value + "K";
}
}
internal UserControl1()
{
InitializeComponent();
}
}
}
namespace FluxController
{
partial class UserControl1
{
private void InitializeComponent()
{
this.Dock = System.Windows.Forms.DockStyle.Fill;
this.Appearance = System.Windows.Forms.Appearance.Button;
this.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment