ChronoWall:マルチモニタ対応のスクリーンセーバーの作成
最近のモニターは焼き付く事はほぼないのですが、待機中も動画が表示されているとかっこいいというだけでスクリーンセーバーを設定しています。しかし、マルチモニターの場合、メインにはスクリーンセーバーが動作しますが、セカンダリーモニターにはスクリーンセーバーがかからず黒い画面になります。スクリーンセーバーは2画面以上に対応していないのがほとんど。
http://www.reallyslick.com/
というようなものもあるのですが、気にいったものが見つけられなかったり有料だったので自作してみました。公開しますが、自分の環境以外では動作を確認していません。
-----------------------------------------------------------------------------
- 時間を映し出す壁:静かに流れる時間を、映像とともに空間に溶け込ませる
ギリシャ語の「時間」から派生したChronoを映す壁という意味で名前をつけてみました。
動作としては、動画をリピートして再生しつづけるというものです。時計代わりに日時も画面に表示するようにしてあります。
-----------------------------------------------------------------------------
【画像】
・先にスクリーンセーバーにしたいmp4またはwmvの動画を用意します。
・私は、以下のPIXABAYのサイトから気に入った動画をダウンロードして利用しています。
https://pixabay.com/ja/videos/
・最近のモニターは焼き付く事はほぼないのですが、画面全体が少し動く同じ写真のように静止しつづけるもの以外を選ぶのと、リピートしますので終わりと始めで大きく変わらないものが良いでしょう。まあ、個人で利用する上では、お気に入りのアーティストのMVとかを流してもよいと思います。・すべてのmp4が上手く再生できない事がわかっていますが、対処法はあります。(下の方に記載)
-----------------------------------------------------------------------------
【設定】
・Windowsフォルダに入れてください。
・設定>個人設定>ロック画面>スクリーンセーバー
・ChronoWallを選択
・設定(T)を押して、スクリーンセーバーとして設定する動画と、日時表示の文字の色を指定する設定画面が現れます。
-----------------------------------------------------------------------------【簡単な仕様】
・WPF標準のMediaElementで実装しているため、mp4とwmvの形式の動画を用いる事ができます。その他の動画ファイルは変換が必要です。
・2画面とも同じ動画がリピートされて再生し続けます。画面のDPIを見ていますので、大きさは調整されます。
・3画面以上は自分の環境ではテストできていません。DPIが変わる可能性がありますが、同じ解像度設定であれば正しく表示されるはずです。
【動画が上手く再生されない場合の対処法】
・mp4によっては、片方の画面のみで再生されて、もう一つの画面側が再生されない。または、再生されてもリピートしない事象が発生する場合があります。
・mp4のフォーマットの違いによるものです。
・回避するには、ffmpegを使って変換してやると上手くできる事があります。(基本、これで回避できています)
powershellのコマンドで
ffmpeg -i input.mov -c:v libx264 -c:a aac -movflags +faststart output.mp4
以下、Visial Studioで開発しましたが、その手順を残しておきます。ご興味ある方は・・・。ただ、素人が、それもはじめて、WPFのアプリを作ったので綺麗なソースコードではない点はご了承ください。
[App.xaml]
StartupUri="MainWindow.xaml"削除
<Application x:Class="ChronoWall_screensaver.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ChronoWall_screensaver">
<Application.Resources>
</Application.Resources>
</Application>
--------------------------------------------------------------------------------------------------------
[App.xaml.cs]
using ChronoWall_screensaver;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
// 既存の using ディレクティブの下に以下を追加
namespace ChronoWall
{
/// <summary>
/// App.xaml の相互作用ロジック
/// </summary>
public partial class App : Application
{
public static string VideoPath { get; private set; }
public static string ClockColorHex { get; private set; }
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
string configFolder = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ChronoWall");
string videoPathFile = System.IO.Path.Combine(configFolder, "VideoPath.txt");
string colorFile = System.IO.Path.Combine(configFolder, "ClockColor.txt");
VideoPath = System.IO.File.Exists(videoPathFile)
? System.IO.File.ReadAllText(videoPathFile).Trim()
: null;
ClockColorHex = System.IO.File.Exists(colorFile)
? System.IO.File.ReadAllText(colorFile).Trim()
: "#FFFFFF";
string mode = e.Args.Length > 0 ? e.Args[0].ToLower() : "/s";
if (mode.StartsWith("/c"))
{
var settingsWindow = new SettingsWindow();
settingsWindow.ShowDialog(); // モーダル表示
Application.Current.Shutdown(); // 設定後は終了
}
else if (mode.StartsWith("/p") && e.Args.Length >= 2)
{
if (int.TryParse(e.Args[1], out int hwndInt))
{
IntPtr previewHandle = new IntPtr(hwndInt);
ShowPreview(previewHandle); // ← ここで呼び出す
}
else
{
Shutdown();
}
}
else if (mode.StartsWith("/s"))
{
var mainWindow = new MainWindow();
mainWindow.Show();
}
else
{
Shutdown();
}
}
private void ShowPreview(IntPtr parentHandle)
{
// 親ウィンドウのサイズ取得
if (!GetClientRect(parentHandle, out RECT rect))
{
Shutdown();
return;
}
int width = rect.Right - rect.Left;
int height = rect.Bottom - rect.Top;
var previewWindow = new Window
{
WindowStyle = WindowStyle.None,
ResizeMode = ResizeMode.NoResize,
ShowInTaskbar = false,
Topmost = false,
Width = width,
Height = height,
Background = Brushes.Black,
AllowsTransparency = false,
ShowActivated = false
};
//string videoPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "aquasurface2.mp4");
//string videoPath = WpfApp1_test.Properties.Settings.Default.VideoPath
var mediaElement = new MediaElement
{
Source = new Uri(VideoPath, UriKind.Absolute),
LoadedBehavior = MediaState.Manual,
UnloadedBehavior = MediaState.Manual,
Stretch = Stretch.UniformToFill,
IsMuted = true
};
mediaElement.MediaOpened += (s, e) =>
{
mediaElement.Position = TimeSpan.Zero;
mediaElement.Play();
};
mediaElement.MediaEnded += (s, e) =>
{
mediaElement.Position = TimeSpan.Zero;
mediaElement.Play();
};
var grid = new Grid();
grid.Children.Add(mediaElement);
previewWindow.Content = grid;
var interopHelper = new WindowInteropHelper(previewWindow);
previewWindow.SourceInitialized += (s, e) =>
{
SetParent(interopHelper.Handle, parentHandle);
MoveWindow(interopHelper.Handle, 0, 0, width, height, true);
};
// サイズ変更に追従するタイマー
var timer = new System.Windows.Threading.DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(500)
};
timer.Tick += (s, e) =>
{
// 他のスクリーンセーバーが選択されてプレビュー領域を奪った場合、自ウィンドウが親の子でなくなる
if (GetParent(interopHelper.Handle) != parentHandle)
{
previewWindow.Close();
Shutdown();
return;
}
if (GetClientRect(parentHandle, out RECT r))
{
int w = r.Right - r.Left;
int h = r.Bottom - r.Top;
MoveWindow(interopHelper.Handle, 0, 0, w, h, true);
}
};
timer.Start();
previewWindow.Show();
mediaElement.Play(); // 念のため明示的に再生
}
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[DllImport("user32.dll")]
private static extern IntPtr GetParent(IntPtr hWnd);
}
}
--------------------------------------------------------------------------------------------------------
【MainWindow.xmal】
<Window x:Class="ChronoWall.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:local="clr-namespace:ChronoWall"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"
Loaded="Window_Loaded">
<Grid>
<!-- 表示要素なし。Window_Loadedで動画再生ウィンドウを生成 -->
</Grid>
</Window>
コメント
コメントを投稿