ChronoWall:マルチモニタ対応のスクリーンセーバーの作成

最近のモニターは焼き付く事はほぼないのですが、待機中も動画が表示されているとかっこいいというだけでスクリーンセーバーを設定しています。しかし、マルチモニターの場合、メインにはスクリーンセーバーが動作しますが、セカンダリーモニターにはスクリーンセーバーがかからず黒い画面になります。スクリーンセーバーは2画面以上に対応していないのがほとんど。

http://www.reallyslick.com/

というようなものもあるのですが、気にいったものが見つけられなかったり有料だったので自作してみました。公開しますが、自分の環境以外では動作を確認していません。

-----------------------------------------------------------------------------

ChronoWall

- 時間を映し出す壁:静かに流れる時間を、映像とともに空間に溶け込ませる

ギリシャ語の「時間」から派生した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

input.movの部分は入力の動画として、output.mp4は出力のファイル名に置き換えます。

・ffmpegをインストールするには、powershellで
winget install ffmpeg
を回すと簡単にインストールできます。

・何を動画として、設定して、色がどれを設定しているかについてはtxtファイルとして保存するようにしています。windows”汚さない”ようにするためです。

-----------------------------------------------------------------------------

以下、Visial Studioで開発しましたが、その手順を残しておきます。ご興味ある方は・・・。ただ、素人が、それもはじめて、WPFのアプリを作ったので綺麗なソースコードではない点はご了承ください。

使ったと言ってもコードは一つも自分では書いていません。copilotにやりたい事をひたすら入力、エラーが出たらエラーコードを入力、時にソースコードをそのまま貼り付けて、変更すべき点を修正させてという方法で作っています



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

--------------------------------------------------------------------------------------------------------
【MainWindow.xmal.cs】
using System;
using System.Data.SqlTypes;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms; // WinFormsのScreenを使うため必要
using System.Windows.Interop; // DPI補正のため必要
using System.Windows.Media;
using System.Windows.Media.Effects;


namespace ChronoWall
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            try
            {
                this.WindowStyle = WindowStyle.None;
                this.WindowState = WindowState.Minimized;
                this.ShowInTaskbar = false;
                this.Topmost = false;
                this.Visibility = Visibility.Hidden;

                string videoPath = App.VideoPath;
                string colorHex = App.ClockColorHex;


                // パスが未設定またはファイルが存在しない場合、デフォルト動画にフォールバック
                if (string.IsNullOrEmpty(videoPath) || !System.IO.File.Exists(videoPath))
                {
                    string fallbackPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "aquasurface2.mp4");

                    if (System.IO.File.Exists(fallbackPath))
                    {
                        videoPath = fallbackPath;
                    }
                    else
                    {
                        System.Windows.MessageBox.Show($"動画ファイルが見つかりません。\n設定されたパス: {App.VideoPath}\nデフォルトパス: {fallbackPath}");
                        System.Windows.Application.Current.Shutdown();
                        return;
                    }
                }


                double dpiX = 1.0, dpiY = 1.0;
                try
                {
                    var source = PresentationSource.FromVisual(this);
                    if (source?.CompositionTarget != null)
                    {
                        dpiX = source.CompositionTarget.TransformFromDevice.M11;
                        dpiY = source.CompositionTarget.TransformFromDevice.M22;
                    }
                }
                catch { dpiX = dpiY = 1.0; }

                foreach (var screen in Screen.AllScreens)
                {
                    var window = new Window
                    {
                        WindowStyle = WindowStyle.None,
                        WindowStartupLocation = WindowStartupLocation.Manual,
                        Left = screen.Bounds.Left * dpiX,
                        Top = screen.Bounds.Top * dpiY,
                        Width = screen.Bounds.Width * dpiX,
                        Height = screen.Bounds.Height * dpiY,
                        ResizeMode = ResizeMode.NoResize,
                        Topmost = true,
                        Background = Brushes.Black,
                        ShowInTaskbar = false,
                        Focusable = true
                    };

                    var mediaElement = new MediaElement
                    {
                        Source = new Uri(videoPath, UriKind.Absolute),
                        LoadedBehavior = MediaState.Play,
                        UnloadedBehavior = MediaState.Manual,
                        Stretch = Stretch.UniformToFill,
                        IsMuted = true
                    };

                    mediaElement.MediaOpened += (s, ev) =>
                    {
                        mediaElement.Position = TimeSpan.Zero;
                        mediaElement.Play();
                    };

                    mediaElement.MediaFailed += (s, ev) =>
                    {
                        System.Windows.MessageBox.Show($"再生失敗: {ev.ErrorException?.Message}");
                    };

                    mediaElement.MediaEnded += (s, ev) =>
                    {
                        mediaElement.Position = TimeSpan.Zero;
                        mediaElement.Play();
                    };

                    var grid = new Grid();
                    grid.Children.Add(mediaElement);

                    // 🕒 時計表示の追加
                    var clockText = new TextBlock
                    {
                        FontSize = 48,
                        FontFamily = new FontFamily(new Uri("pack://application:,,,/"), "./Fonts/#Orbitron"),
                        Foreground = Brushes.White,
                        HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
                        VerticalAlignment = System.Windows.VerticalAlignment.Bottom,
                        Margin = new Thickness(20),
                        Text = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"),
                        FontWeight = FontWeights.Bold
                    };

                    if (!string.IsNullOrEmpty(colorHex))
                    {
                        var brushConverter = new BrushConverter();
                        try
                        {
                            clockText.Foreground = (Brush)brushConverter.ConvertFromString(colorHex);
                        }
                        catch
                        {
                            clockText.Foreground = Brushes.White;
                        }
                    }


                    clockText.Effect = new DropShadowEffect
                    {
                        BlurRadius = 15,
                        ShadowDepth = 0,
                        Opacity = 0.8

                    };

                    var clockPanel = new Border
                    {
                        Background = new SolidColorBrush(Color.FromArgb(80, 0, 0, 40)), // 半透明ダークブルー
                        BorderBrush = Brushes.Cyan,
                        BorderThickness = new Thickness(0),
                        CornerRadius = new CornerRadius(10),
                        Padding = new Thickness(10),
                        HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
                        VerticalAlignment = VerticalAlignment.Bottom,
                        Margin = new Thickness(20),
                        Child = clockText

                    };

                    grid.Children.Add(clockPanel);

                    var timer = new System.Windows.Threading.DispatcherTimer
                    {
                        Interval = TimeSpan.FromSeconds(1)
                    };
                    timer.Tick += (s, ev) =>
                    {
                        clockText.Text = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss");
                    };
                    timer.Start();

                    window.Content = grid;

                    // イベント制御用変数(foreach内で宣言)
                    DateTime initializedAt = DateTime.MinValue;
                    Point initialMousePosition = new Point();
                    bool mouseInitialized = false;

                    window.Loaded += (s, ev) =>
                    {
                        initializedAt = DateTime.Now;
                        initialMousePosition = System.Windows.Input.Mouse.GetPosition(window);
                        mouseInitialized = true;
                    };

                    window.MouseMove += (s, ev) =>
                    {
                        if (!mouseInitialized || (DateTime.Now - initializedAt).TotalSeconds < 1) return;

                        Point current = System.Windows.Input.Mouse.GetPosition(window);
                        if (Math.Abs(current.X - initialMousePosition.X) > 10 ||
                            Math.Abs(current.Y - initialMousePosition.Y) > 10)
                        {
                            System.Windows.Application.Current.Shutdown();
                        }
                    };

                    window.MouseDown += (s, ev) =>
                    {
                        if ((DateTime.Now - initializedAt).TotalSeconds >= 1)
                            System.Windows.Application.Current.Shutdown();
                    };

                    window.PreviewKeyDown += (s, ev) =>
                    {
                        if ((DateTime.Now - initializedAt).TotalSeconds < 1) return;

                        System.Windows.Application.Current.Shutdown(); // どのキーでも終了
                    };


                    window.Show();
                    window.Activate(); // フォーカスを明示的に取得
                    window.Focus(); // ← 追加

                }
            }
            catch (Exception ex)
            {
                System.Windows.MessageBox.Show("MainWindow 初期化エラー: " + ex.Message);
                System.Windows.Application.Current.Shutdown();
            }
        }
    }
}
--------------------------------------------------------------------------------------------------------

SettingsWindow.xamlを追加

System.DrawingとSystem.Windows.Formsにチェックを入れる


--------------------------------------------------------------------------------------------------------
【SettingsWindow.xmal】 

<Window x:Class="ChronoWall.SettingsWindow"
        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="スクリーンセーバー設定" Height="250" Width="400">
    <StackPanel Margin="10">
        <!-- 動画選択 -->
        <TextBlock Text="動画ファイルを選択してください:" Margin="0,0,0,10"/>
        <Button Content="動画を選択" Click="SelectVideo_Click" Width="120"/>
        <TextBlock x:Name="SelectedPathText" Margin="0,10,0,20"/>

        <!-- 時計色選択 -->
        <TextBlock Text="時計の文字色を選択してください:" Margin="0,0,0,5"/>
        <Button Content="色を選択" Click="SelectColor_Click" Width="120"/>
        <TextBlock x:Name="SelectedColorText" Margin="0,5,0,10"/>

        <!-- 保存 -->
        <Button Content="Close" Click="Save_Click" Width="80" HorizontalAlignment="Right"/>
    </StackPanel>
</Window>

--------------------------------------------------------------------------------------------------------

【SettingsWindow.xmal.cs】 
using System;
using System.Windows;
using System.Windows.Forms; // ColorDialog 用
using System.Windows.Media;
using System.IO;       


namespace ChronoWall
{
    public partial class SettingsWindow : Window
    {
        string configFolder = System.IO.Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "ChronoWall");


        public SettingsWindow()
        {
            InitializeComponent();

            System.IO.Directory.CreateDirectory(configFolder); // フォルダーがなければ作成

            string videoPathFile = System.IO.Path.Combine(configFolder, "VideoPath.txt");
            string colorFile = System.IO.Path.Combine(configFolder, "ClockColor.txt");

            // 動画パスの読み込み(外部ファイル)
            if (System.IO.File.Exists(videoPathFile))
            {
                SelectedPathText.Text = System.IO.File.ReadAllText(videoPathFile);
            }
            else
            {
                SelectedPathText.Text = "(未設定)";
            }

            // 時計色の読み込み(外部ファイル)
            if (System.IO.File.Exists(colorFile))
            {
                string colorHex = System.IO.File.ReadAllText(colorFile).Trim();
                SelectedColorText.Text = $"選択された色: {colorHex}";
            }
            else
            {
                SelectedColorText.Text = "(未設定)";
            }
        }

        private void SelectVideo_Click(object sender, RoutedEventArgs e)
        {

            string videoPathFile = System.IO.Path.Combine(configFolder, "VideoPath.txt");
            string colorFile = System.IO.Path.Combine(configFolder, "ClockColor.txt");
            var dialog = new Microsoft.Win32.OpenFileDialog
            {
                Filter = "動画ファイル (*.mp4;*.wmv)|*.mp4;*.wmv",
                Title = "動画ファイルを選択"
            };

            if (dialog.ShowDialog() == true)
            {
                System.IO.File.WriteAllText(videoPathFile, dialog.FileName);
                SelectedPathText.Text = dialog.FileName;
            }
        }

        private void SelectColor_Click(object sender, RoutedEventArgs e)
        {

            string videoPathFile = System.IO.Path.Combine(configFolder, "VideoPath.txt");
            string colorFile = System.IO.Path.Combine(configFolder, "ClockColor.txt");
            var colorDialog = new ColorDialog();

            // 既存色の読み込み
            if (System.IO.File.Exists(colorFile))
            {
                try
                {
                    var currentColor = System.Drawing.ColorTranslator.FromHtml(
                        System.IO.File.ReadAllText(colorFile).Trim());
                    colorDialog.Color = currentColor;
                }
                catch { }
            }

            if (colorDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                string hex = $"#{colorDialog.Color.R:X2}{colorDialog.Color.G:X2}{colorDialog.Color.B:X2}";
                System.IO.File.WriteAllText(colorFile, hex);
                SelectedColorText.Text = $"選択された色: {hex}";
            }
        }

        private void Save_Click(object sender, RoutedEventArgs e)
        {
            this.Close(); // 外部ファイルに保存済みなので、Settings.Default.Save() は不要
        }
    }
}

コメント

このブログの人気の投稿

Attiny85とAQM0802A(LCD)のI2C接続

CH9329で日本語キーボード109で正しく表示する方法