【ツール作り】ちょっと便利なスクショツールを作りたい#2【C#】

スポンサーリンク
C#
スポンサーリンク

はじめに

最終的な目標は便利なスクショツールを作ること。現在は、マウスクリックだけでスクショが撮れるツールを作っています。

前回の記事で次のような単純なものが作れました。

  • 背景を薄暗くしてスクショ撮る感を出す
  • 画面のクリックで全画面のスクショを撮る

今回は指定したウィンドウと自由に矩形範囲を選択してスクショを撮れるようにしていきます。

3種類のスクショで撮影できるようにする

どこを共通化するか等、まったく考えていなかったので、ほぼ1から作り直しています。。。

「MainWindow.xaml」に追加していた「PreviewMouseDown」イベントは次の理由で削除。

  • イベントの発生がrootなのが扱いづらい(Preview系は目的に適してなさそう)
  • 左クリックだけ有効の方が操作性が良さそう(LeftMouseDownとかがよさげ)
  • xamlでイベント登録するのが意外と手間(全部C#でやります)
ウィンドウに付けてたイベントを削除

作成した「MainWindow.xaml.cs」の全体

どこを変えるというレベルでは追いつかないので、まずは作成した「MainWindow.xaml.cs」 のコード全体を載せておきます。

とりあえずコピペして実行すれば動く。。。はず。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Image = System.Windows.Controls.Image;
using Point = System.Drawing.Point;

namespace ClickScreenShot
{
    public partial class MainWindow : Window
    {
        //固定値
        private const int DWMWA_EXTENDED_FRAME_BOUNDS = 9;
        private const int DWMWA_CLOAKED = 14;
        private const double BUTTON_SCALE = 20;                     //ボタンサイズ(縮小率)
        private const double THICKNESS_SIZE = 7;                    //WPF全画面表示バグ対応
        private const int SCREEN_MODE = 0;                          //全画面領域モード
        private const int WINDOW_MODE = 1;                          //ウィンドウ領域モード
        private const int RECTANGLE_MODE = 2;                       //矩形領域モード
        private const int ON_CLICK = 0;                             //押下中
        private const int OFF_CLICK = 1;                            //離上中

        //ウィンドウ領域リスト
        private static List<Rectangle> WindowRectList { get; set; }

        //キャプチャデータ
        private Bitmap CaptureBitmap { get; set; }                          

        //状態変数
        private int ModeStatus { get; set; }                        //動作モード
        private int ClickStatus { get; set; }                       //押下状態
        private Rectangle CaptureRect { get; set; }                 //撮影領域

        //UI画面表示
        private Canvas UICanvas { get; set; }                       //UI画面
        private Button ScreenButton { get; set; }                   //全画面領域
        private Button WindowButton { get; set; }                   //ウィンドウ領域
        private Button RectangleButton { get; set; }                //矩形領域
        private Image CaptureImage { set; get; }                    //撮影領域画像

        //ウィンドウ取得
        [StructLayout(LayoutKind.Sequential)]
        private struct Rect
        {
            public int left;
            public int top;
            public int right;
            public int bottom;
        }

        [DllImport("user32.dll")]
        private static extern bool EnumWindows(WNDENUMPROC lpEnumFunc, IntPtr lParam);
        private delegate bool WNDENUMPROC(IntPtr hWnd, IntPtr lParam);
        private static bool EnumerateWindows(IntPtr hWnd, IntPtr lParam)
        {
            //例外処理
            if (!IsWindowVisible(hWnd))
            {
                return true;
            }

            DwmGetWindowAttribute(hWnd, DWMWA_CLOAKED, out bool IsCloaked, Marshal.SizeOf(typeof(bool)));
            if (IsCloaked == true)
            {
                return true;
            }

            //一時変数
            Rectangle WindowRect = new Rectangle();

            //ウィンドウ領域の取得
            DwmGetWindowAttribute(hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, out Rect ExtendedRect, Marshal.SizeOf(typeof(Rect)));

            WindowRect.X = ExtendedRect.left;
            WindowRect.Y = ExtendedRect.top;
            WindowRect.Width = ExtendedRect.right - ExtendedRect.left;
            WindowRect.Height = ExtendedRect.bottom - ExtendedRect.top;

            //例外処理
            if (WindowRect.X < 0 || WindowRect.Y < 0 || WindowRect.Width == 0 || WindowRect.Height == 0)
            {
                return true;
            }

            //ウィンドウ領域リストの更新
            WindowRectList.Add(WindowRect);

            //処理継続
            return true;
        }

        [DllImport("user32.dll")]
        private static extern bool IsWindowVisible(IntPtr hWnd);

        [DllImport("dwmapi.dll")]
        extern static int DwmGetWindowAttribute(IntPtr hWnd, int dwAttribute, out Rect pvAttribute, int cbAttribute);

        [DllImport("dwmapi.dll")]
        extern static int DwmGetWindowAttribute(IntPtr hWnd, int dwAttribute, out bool pvAttribute, int cbAttribute);

        public MainWindow()
        {
            InitializeComponent();

            //ウィンドウ領域リストを初期化
            WindowRectList = new List<Rectangle>();
            EnumWindows(EnumerateWindows, IntPtr.Zero);

            //スタイル
            WindowStyle = WindowStyle.None;                         //境界線,ボタンの非表示
            WindowState = WindowState.Maximized;                    //全画面表示
            BorderThickness = new Thickness(THICKNESS_SIZE);        //WPF全画面表示バグ対応
            Topmost = true;                                         //最前面表示

            //背景
            AllowsTransparency = true;                              //透過許可
            Background = new SolidColorBrush(Colors.Transparent);   //透過

            //キャプチャデータの初期化
            InitCapture();

            //状態変数初期化
            ModeStatus = SCREEN_MODE;
            ClickStatus = OFF_CLICK;
            CaptureRect = new Rectangle();

            //UI画面初期化
            InitUI();
            this.Content = UICanvas;                                //ウィンドウにUI画面を追加

            //モード選択ボタン更新
            ModeButtonUpdate();
        }

        //キャプチャデータの初期化
        private void InitCapture()
        {
            //全画面領域のオブジェクト生成
            CaptureBitmap = new Bitmap((int)SystemParameters.VirtualScreenWidth, (int)SystemParameters.VirtualScreenHeight);
            Graphics ScreenGraphics = Graphics.FromImage(CaptureBitmap);

            //全画面領域の画像をコピー
            ScreenGraphics.CopyFromScreen(new Point(0, 0), new Point(0, 0), CaptureBitmap.Size);
            ScreenGraphics.Dispose();
        }

        //UI画面初期化
        private void InitUI() 
        {
            //UI画面生成
            UICanvas = new Canvas
            {
                Background = new SolidColorBrush                    //背景色の設定
                {
                    Color = Colors.Black,
                    Opacity = 0.5
                }
            };
            UICanvas.MouseLeftButtonDown += new MouseButtonEventHandler(UILeftMouseDown);
            UICanvas.MouseLeftButtonUp += new MouseButtonEventHandler(UILeftMouseUp);
            UICanvas.MouseMove += new MouseEventHandler(UIMouseMove);

            //UI画面に画像表示を追加
            CaptureImage = new Image();
            UICanvas.Children.Add(CaptureImage);

            //ボタンサイズの算出
            double WorkW = SystemParameters.WorkArea.Width;         //スクリーンサイズ取得
            double WorkH = SystemParameters.WorkArea.Height;        //スクリーンサイズ取得
            double ButtonSize = (WorkW < WorkH) ? WorkW : WorkH;    //ウィンドウの縦横小さい値を取得
            ButtonSize /= BUTTON_SCALE;                             //取得した値を縮小

            //ボタン生成
            //全画面領域
            ScreenButton = new Button
            {
                Content = "Screen",
                Width = ButtonSize,
                Height = ButtonSize,
                Tag = SCREEN_MODE
            };
            ScreenButton.Click += new RoutedEventHandler(ModeButtonClick);

            //ウィンドウ領域
            WindowButton = new Button
            {
                Content = "Window",
                Width = ButtonSize,
                Height = ButtonSize,
                Tag = WINDOW_MODE
            };
            WindowButton.Click += new RoutedEventHandler(ModeButtonClick);

            //矩形領域
            RectangleButton = new Button
            {
                Content = "Rect",
                Width = ButtonSize,
                Height = ButtonSize,
                Tag = RECTANGLE_MODE
            };
            RectangleButton.Click += new RoutedEventHandler(ModeButtonClick);

            //ボタン配置
            Canvas.SetLeft(ScreenButton, WorkW - ButtonSize);
            Canvas.SetLeft(WindowButton, WorkW - ButtonSize);
            Canvas.SetLeft(RectangleButton, WorkW - ButtonSize);

            Canvas.SetTop(ScreenButton, WorkH / 2 - ButtonSize * 1.5);
            Canvas.SetTop(WindowButton, WorkH / 2 - ButtonSize * 0.5);
            Canvas.SetTop(RectangleButton, WorkH / 2 + ButtonSize * 0.5);
            
            //UI画面にボタンを追加
            UICanvas.Children.Add(ScreenButton);
            UICanvas.Children.Add(WindowButton);
            UICanvas.Children.Add(RectangleButton);
        }

        //モード選択ボタン更新
        private void ModeButtonUpdate()
        {
            //選択可能色
            SolidColorBrush SelectableColor = new SolidColorBrush
            {
                Color = Colors.White,
                Opacity = 0.5
            };

            //選択不可能色
            SolidColorBrush UnSelectableColor = new SolidColorBrush
            {
                Color = Colors.Black,
                Opacity = 0.5
            };

            //ボタンの色を更新
            ScreenButton.Background = ((int)ScreenButton.Tag == ModeStatus) ? UnSelectableColor : SelectableColor;
            WindowButton.Background = ((int)WindowButton.Tag == ModeStatus) ? UnSelectableColor : SelectableColor;
            RectangleButton.Background = ((int)RectangleButton.Tag == ModeStatus) ? UnSelectableColor : SelectableColor;
        }

        //ウィンドウ領域検索
        private bool SetWindowRect(int X, int Y)
        {
            foreach(Rectangle RectItem in WindowRectList)
            {
                if(RectItem.X <= X && X <= RectItem.X + RectItem.Width &&
                   RectItem.Y <= Y && Y <= RectItem.Y + RectItem.Height)
                {
                    CaptureRect = RectItem;
                    return true;
                }
            }
            return false;
        }

        //キャプチャの描画
        private void DrawCapture()
        {
            //矩形領域の複製
            Rectangle DrawRect = CaptureRect;

            //例外処理
            if(DrawRect.Width == 0 || DrawRect.Height == 0)
            {
                return;
            }

            //矩形領域の調整
            if(DrawRect.Width < 0)
            {
                DrawRect.X += DrawRect.Width;
                DrawRect.Width *= -1;
            }
            if(DrawRect.Height < 0)
            {
                DrawRect.Y += DrawRect.Height;
                DrawRect.Height *= -1;
            }

            //画像の描画
            Bitmap DrawBitmap = CaptureBitmap.Clone(DrawRect, CaptureBitmap.PixelFormat);
            using (var stream = new MemoryStream())
            {
                DrawBitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
                stream.Seek(0, SeekOrigin.Begin);
                CaptureImage.Source = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
            }
            Canvas.SetLeft(CaptureImage, DrawRect.X);
            Canvas.SetTop(CaptureImage, DrawRect.Y);
        }

        //キャプチャの保存
        private void SaveCapture()
        {
            //矩形領域の複製
            Rectangle SaveRect = CaptureRect;

            //例外処理
            if (SaveRect.Width == 0 || SaveRect.Height == 0)
            {
                return;
            }

            //矩形領域の調整
            if (SaveRect.Width < 0)
            {
                SaveRect.X += SaveRect.Width;
                SaveRect.Width *= -1;
            }
            if (SaveRect.Height < 0)
            {
                SaveRect.Y += SaveRect.Height;
                SaveRect.Height *= -1;
            }

            //画像の保存
            Bitmap SaveBitmap = CaptureBitmap.Clone(SaveRect, CaptureBitmap.PixelFormat);
            SaveBitmap.Save(@"C:\Users\K_tora\Downloads\capture.png", System.Drawing.Imaging.ImageFormat.Png);
        }

        //モード選択ボタン押下イベント
        private void ModeButtonClick(object sender, RoutedEventArgs e)
        {
            //ボタンに応じてモードを更新
            Button ClickButton = (Button)sender;
            ModeStatus = (int)ClickButton.Tag;

            //キャプチャの削除
            CaptureImage.Source = null;

            //モード選択ボタン更新
            ModeButtonUpdate();
        }

        //UI画面押下イベント
        private void UILeftMouseDown(object sender, MouseButtonEventArgs e)
        {
            //押下状態の更新
            ClickStatus = ON_CLICK;

            //モードで分岐
            switch (ModeStatus) {
                case SCREEN_MODE:
                    //矩形領域を更新
                    Rectangle TempRect = CaptureRect;
                    TempRect.X = 0;
                    TempRect.Y = 0;
                    TempRect.Width = (int)SystemParameters.VirtualScreenWidth;
                    TempRect.Height = (int)SystemParameters.VirtualScreenHeight;
                    CaptureRect = TempRect;

                    //キャプチャの保存
                    SaveCapture();
                    Close();

                    break;
                case WINDOW_MODE:
                    //キャプチャの保存
                    SaveCapture();
                    Close();
                    
                    break;
                case RECTANGLE_MODE:
                    //矩形領域を更新
                    Rectangle StartPos = CaptureRect;
                    StartPos.X = (int)e.GetPosition(UICanvas).X;
                    StartPos.Y = (int)e.GetPosition(UICanvas).Y;
                    CaptureRect = StartPos;
                    
                    break;
                default:
                    break;
            }
        }

        //マウス移動イベント
        private void UIMouseMove(object sender, MouseEventArgs e)
        {
            //モードで分岐
            switch (ModeStatus)
            {
                case WINDOW_MODE:
                    //離上中のみ実行
                    if (ClickStatus == ON_CLICK)
                    {
                        break;
                    }

                    //矩形領域の更新
                    if(SetWindowRect((int)e.GetPosition(UICanvas).X, (int)e.GetPosition(UICanvas).Y))
                    {
                        //キャプチャの描画
                        DrawCapture();
                    }

                    break;
                case RECTANGLE_MODE:
                    //押下中のみ実行
                    if (ClickStatus == OFF_CLICK)
                    {
                        break;
                    }

                    //矩形領域を更新
                    Rectangle StartPos = CaptureRect;
                    StartPos.Width = (int)e.GetPosition(UICanvas).X - StartPos.X;
                    StartPos.Height = (int)e.GetPosition(UICanvas).Y - StartPos.Y; 
                    CaptureRect = StartPos;

                    //キャプチャの描画
                    DrawCapture();
                    
                    break;
                default:
                    break;
            }
        }  

        //UI画面離上イベント
        private void UILeftMouseUp(object sender, MouseButtonEventArgs e)
        {
            //押下状態の更新
            ClickStatus = OFF_CLICK;

            //モードで分岐
            switch (ModeStatus)
            {
                case RECTANGLE_MODE:
                    //矩形領域を更新
                    Rectangle StartPos = CaptureRect;
                    StartPos.Width = (int)e.GetPosition(UICanvas).X - StartPos.X;
                    StartPos.Height = (int)e.GetPosition(UICanvas).Y - StartPos.Y;
                    CaptureRect = StartPos;

                    //キャプチャの保存
                    SaveCapture();
                    Close();

                    break;
                default:
                    break;
            }

            //異常:画面を閉じる
            Close();
        }
    }
}

変更点が多すぎて全部説明するわけにはいかないため、以降はやや特殊な点についてのみ説明していきます。

WPFの全画面表示バグ

WPFで全画面表示すると6~8pxほど、実際の画面より大きいウィンドウが生成されるらしい。

このままだと座標系が微妙にずれて使えないので、ウィンドウ枠を付けることで補正した。

BorderThickness = new Thickness(THICKNESS_SIZE);        //WPF全画面表示バグ対応

これが正しい対処法なのかは不明。

他ウィンドウ領域の取得

他ウィンドウ領域の取得はAPIの関数を利用した。「EnumWindows」でウィンドウの列挙を行い、ウィンドウ領域をリストに格納する。

利用したAPIの関数は次の通り。

  • EnumWindows:有効なメインウィンドウの列挙
  • IsWindowVisible:ウィンドウが可視状態(WS_VISIBLE)であるかを判定する
  • DwmGetWindowAttribute:ウィンドウを見た目通りにキャプチャする/クローク状態のウィンドウであるか判定する

「DwmGetWindowAttribute」は2通りの使われ方をしています。

後者のクローク状態のウィンドウであるか判定するのは、「IsWindowVisible」だけではユーザーには見えないのに「WS_VISIBLE」であるウィンドウを取り除けないためです。

動作確認

それぞれを動作確認して、既存のスクショ機能で撮影(この時点でスクショツールの必要性をやや疑う)してみた。

全画面領域

実行と同時に全体が暗くなり、クリックでキャプチャ。

キャプチャ画面(全画面領域)
保存された画像(全画面領域)

ウィンドウ領域

マウスをウィンドウに合わせるとハイライトされ、クリックでキャプチャ。

キャプチャ画面(ウィンドウ領域)
保存された画像(ウィンドウ領域)

矩形領域

左クリック押下→移動させると範囲がハイライト→左クリック離上でキャプチャ。

キャプチャ画面(矩形領域)
保存された画像(矩形領域)

ちなみに上の画像の状態で、既存のスクショ機能を実行すると離上イベントが拾えずに終わってしまうため、スクショが上手く撮影されない。スクショ中をスクショするような奇行はされないと信じてる。

最後に

やや不具合が見つかったものの、問題ない範囲。

スクショツールについて残っているのは、タスクトレイ常駐でクリックで実行する部分だけかな?次回でスクショツールは完成予定。

コメント

タイトルとURLをコピーしました