Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

32bit UEFIブートかつ64bitアーキテクチャにubuntuをインストールするまで

先日部屋掃除をしていたところ、
昔使用していた2in1タブレットPC(Aspire Switch)を発見!!
懐かしくなり起動させてみたところ、OSがWindows8.1。
せっかくなのでwindows10にアップデートしようとしたのですが、なぜかインストール失敗。。
まぁ、windows10がインストールできたとしてもスペック的にカクついたりしてまともに動かなかっただろう。

このまま眠らせておくのももったいないので、、
せっかくなのでubuntuをインストールすることに!!

下記からubuntuUSBメモリに焼いてインストールすればよいのだと思い気軽な気持ちで始めたのですが、
ちょっと手間取ったところがあったのでこちらに方法をまとめたいと思います。
Ubuntu 22.04 LTS (Jammy Jellyfish)



■手間取った点

どうやらこちらのタブレット
64bitアーキテクチャだがブートが32bit UEFIブートと特殊な環境。。
このため、64bit版Ubuntuをインストールしようとしてもブートが異なるため、インストーラが動かせない。

こんな環境でUbuntuインストール出来るのか?
と思い、色々調べたところ、こちらの記事を発見!!
インストールするにあたり参考にさせて頂くことにしました!!
blog.goediy.com
blog.goediy.com

■解決策

こちらの記事に記載されている通り、
ブートローダにbootia32.efiをインストールメディアに含めればOKです。
インストールメディアで起動させる際にbootia32.efiでブートするもよう。

bootia32.efiは、インストールメディアの/EFI/boot配下に格納すればOK。
すでに64bit用のefiファイルが格納されているかと思いますが、一緒に格納しておけばOK。

bootia32.efiは下記手順を参考にすれば作成可能です。
Ubuntu on Acer Aspire Switch 10 · GitHub

、、、が、面倒な方はこちらからダウンロードしてもOKです。
linux-asus-t100ta/bootia32.efi at master · jfwells/linux-asus-t100ta · GitHub

■インストール手順

基本的にはこちらの手順を元に実施すればOKなのですが、1点だけ補足。
blog.goediy.com

USBメモリへのインストールメディアコピーですが、
rufus(Rufus - 起動可能なUSBドライブを簡単に作成できます)
を用いるとwindows上のエクスプローラEFI/boot配下のフォルダが開けるのでbootia32.efiを入れるのがとても簡単でした。
Linux環境でddでUSBメモリに書き込みを行ってしまうとbootia32.efiを書き込むためにマウントしなければならないので、、、
そんな手間がrufusだとかからないので是非。

後、インストール後のUbuntuをブートする際のgrubコマンドですが、
①lsコマンドでどのパーティションが存在するかチェック
②下記コマンドでデバイスパーティションをset

grub> set root=(hd*,gpt*)

③下記コマンドでパーティション内に何が入っているのかチェック

ls /

すると、間違ったデバイスパーティションでbootしなくてよく、混乱も少ないかと思います。

合わせて、、、
今回ご紹介した手順ですと、こちらのコマンドでインストールしていますが、

sudo ubiquity -b

普通にインストールを選択してインストールを開始してしまってもよさそうです。
32bit UEFI搭載の2in1にLinuxをぶち込んだお話 | ちゃろのガジェット日記


自分の環境ですと、
grubが上手く動作せずUSBメモリを抜いてしまうと"No Bootable device"と出てしまったのでコマンドで実行しましたが。

■最後に

grubコマンドは触ったことがなかったので今回とても勉強になりました。
普通にUbuntuインストールだけだと、ブートなど意識しなくても立ち上がってくれるので。

今回を機に、grubコマンドにも慣れ親しんでみたいと思います。



javascriptをtype="module"で読み出すとonclickイベントで関数がis not definedになる

先日調査したjsonデータ⇔csvデータ変換やjsonデータ⇔xmlデータ変換や各種ファイルを出力する方法をまとめました。
elsammit-beginnerblg.hatenablog.com
elsammit-beginnerblg.hatenablog.com

どうせなのでこれらのコードをgithub pagesで公開したら後で便利かな?
と思い立ち、Webアプリを作成中!!

そこで、
ファイル分割のためにexport/importを使用したところ、
ちょっとハマったことがあるので備忘録としてまとめたいと思います。



■export/importを用いてファイル分割を行ってみる。

javascriptでは、export/importを用いることで、別ファイルで定義した関数を使用することが出来るようになります。

例えば、hoge1.jsとhoge2.jsを用意し、足し算を行う関数(add)をhoge2.jsで定義し、hoge1.jsで呼び出したいとします。
その場合には、こちらのように記載すればhoge1.jsでhoge2.jsで定義したadd関数をコール出来ます。
【hoge1.js】

import {add} from './hoge2.js'

const addhandler = () =>{
    alert(add(1,2));
}

【hoge2.js】

export function add(x,y){
    return x+y;
}

この際、htmlファイルではhoge1.jsを呼び出す必要があるのですが、
こちらのコードの通り、

<script src="hoge1.js" type="module"></script>

type="module"と定義する必要があります。

htmlファイルのコード。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="hoge1.js" type="module"></script>
        <title>Document</title>
    </head>
    <body>
        hello world
    </body>
</html>

■htmlでtype="module"でjavascriptファイルを読み出すとonclickイベントで関数がis not defined

先ほど記載しました通り、
export/importでファイル分割して別ファイルの関数を呼び出すことができる。
export/importを使用するためには、

<script src="hoge1.js" type="module"></script>

が必要。
となります。

ここで、hoge1.jsとhtmlファイルをこちらのように変更してみます。

import {add} from './hoge2.js'

const addhandler = () =>{
    alert(add(1,2));
}

const ClickFunc = () =>{
    alert('Hello World');
}
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="hoge1.js" type="module"></script>
        <title>Document</title>
    </head>
    <body>
        hello world
        <button type="button" onClick="ClickFunc ()">Click!!(xml)</button>
    </body>
</html>

変更点ですが、
buttonタグを追加してボタンクリックした際に、
onClickイベントによりClickFunc ()がコールされるように追加した
のみになります。

ですが、こちらを実行してみると、、、
ClickFunc が
is not defined
と関数が定義されていない旨のエラーメッセージが表示されます。

■is not definedの原因

type="module"にしたことで”モジュールスコープ”になってしまったことで
is not definedと参照出来ない状況になっていたようです。

モジュールスコープになっていることにより、
モジュール内のすべての宣言は、モジュールにスコープされます。
他のjavasciptがこれらの宣言にアクセスしようとすると、参照エラーがスローされます。

これにより、
hoge1.jsで定義されたClickFunc はhoge1.js内でのみでしか使用できない状態のため、
onClickイベントにて関数が見つけられない(is not defined)になってしまった。
ということです。

■is not definedの解決方法

解決方法ですが、hoge1.jsで定義するClickFunc()に対して下記の通り、
明示的にwindowオブジェクトへ公開すればOKです。

import {add} from './hoge2.js'

const addhandler = () =>{
    alert(add(1,2));
}

window.ClickFunc = () =>{
    alert('Hello World');
}

もしくは、onClickは用いずに
addEventListenerハンドラーを用いればOKです。

■最後に

今回は自分がはまったtype="module"で、
onclickイベントで関数がis not definedになる現象に関して原因と解決方法をまとめました。

今回は、onclickイベントを例に記載しましたが
onloadやonChangeなどでhtmlから呼び出す関数に対して、
今回の解決方法を適用しないとis not definedになってしまいます。
注意しておかないとハマってしまいそうですね。。
気を付けて実装していきます。



ウィンドウ上の指定した領域に対するスクリーン録画アプリをリリースしました

先日、指定した領域に対してスクリーンショット(録画)をする方法をまとめました。
elsammit-beginnerblg.hatenablog.com


今後、自分でも何かと使いそうだな!!と思ったので、
アプリケーションを作成しました。


■アプリダウンロード先

アプリはgithubにて公開いたしました。
ダウンロード先のURLはこちらになります。
https://github.com/Elsammit/ScreenCapture/archive/refs/tags/Version1.0.zip

また、ソースコードgithubにて公開しておりますのでこちらもどうぞ!!
GitHub - Elsammit/ScreenCapture: スクリーンキャプチャ。

■使用条件(動作確認環境)

 ・OS:Windows10 64bit
 ・メモリ:8GB以上
 ・ディスプレイ:デュアルディスプレイ以上で使用ください。
  ※シングルディスプレイですと、
   アプリ上に表示される映像が繰り返されてしまい、正しくスクリーンを録画できなくなります。
 ・ディスプレイ解像度:1920x1080

■アプリ紹介

今回作成したアプリはこちらの通り、
右下に録画用のボタンと中央にスクリーン全体の映像を表示する構成としました。


赤枠が録画を行う領域になります。
こちらはマウスを左クリックしながら動かせば指定することが可能です。

尚、赤枠を指定していない場合にはスクリーン全体を録画する仕様です。


録画開始ボタンを押すと保存先を指定するダイアログが表示され、
ファイルを選択するとこちらの通り、録画画面が表示されます。

。。といっても大きく変わっている点は、
右上にRecと録画時間が表示されるのみなのですが。

また録画指定エリア(赤枠)ですが、録画中は変更できない仕様にしております。

録画を終了する際は右下の録画停止ボタンを押してください。

■アプリを使用する際の注意事項

こちらにアプリを使用する際の注意事項を記載いたします。

【注意時事項】
デュアルディスプレイ以上で使用している場合、プライマリディスプレイのみが録画対象になります。
・録画ファイルのフォーマットはwmv固定です。
・録画時間は最長30分です(30分を超えると強制的に録画終了します)。

■最後に

今回は自分で作成したウィンドウ上の指定した位置のみを録画するアプリの紹介でした。
もし不具合、要望等ございましたらgithubにてご連絡頂ければ適宜対応したいと思います。


WPFでディスクトップ上の指定したエリアのスクリーン録画を行ってみた

自分でカスタマイズしながらスクリーンショットアプリが欲しくて色々試してきました。

今回はこちらで紹介したウィンドウ全体を表示、録画する方法をまとめました。
elsammit-beginnerblg.hatenablog.com

また、Rctangleで描画する方法もまとめました。
elsammit-beginnerblg.hatenablog.com

今回はこちらのディスクトップウィンドウのスクリーン録画する方法と、
Rectangleを描画する方法を組み合わせて、、、
Rectangleで描画した部分を切り出してスクリーン録画する方法をまとめたいと思います。
これで画面上の好きなエリアのみを録画することが出来るようになります。



■完成形イメージ

アプリの完成形ですが、
こちらのように中央にディスクトップウィンドウを表示させ、
マウスで指定エリア指定した後に録画ボタンを押すと指定したエリアを切り出して録画をする。
といった形になります。

ソースコードについて

ソースコードはこちらに公開しております。
コード全体を確認したい方はこちらをご参考ください。
github.com

■画面デザイン(xaml)

画面デザインですが、こちらのように作成しました。
CanvasとImageを同じ大きさ位置で表示するように作成し、Imageに対してBorderで枠線を引いております。
Canvas(& Image)のサイズですが、
Height:720px
Width:1280px
としております。
後はボタンが用意されているのみのシンプルな画面になります。

<Window x:Name="ScreenCapture" x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="ScreenCaptureツール" Height="792.901" Width="1485.972" Closing="CloseWindow" Loaded="WindowLoad"
        ResizeMode="NoResize" Icon="DispIcon.jpeg" Background="#FF5D5D5D">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Canvas x:Name="RectArea" HorizontalAlignment="Left" Margin="17,18,0,0" VerticalAlignment="Top" Height="720"  Width="1280" MouseLeftButtonDown="MouseLeftBtnDwn" MouseMove="MouseMoving" MouseLeftButtonUp="MouseLeftBtnUp" Panel.ZIndex="1" Background="Transparent" OpacityMask="Gray"/>
        <Border BorderBrush="White" BorderThickness="1" HorizontalAlignment="Left" Height="720" Margin="17,18,0,0" VerticalAlignment="Top" Width="1280">
            <Image x:Name="ImgCap" HorizontalAlignment="Left" Height="720" VerticalAlignment="Top" Width="1280" Margin="0,0,0,0"/>
        </Border>
        <Button x:Name="StartButton" Content="録画開始" HorizontalAlignment="Left" Height="74" Margin="1317,659,0,0" VerticalAlignment="Top" Width="136" Click="Button_Click" RenderTransformOrigin="1.155,1.833" Panel.ZIndex="2" FontSize="24">
            <Button.RenderTransform>
                <TransformGroup>
                    <ScaleTransform/>
                    <SkewTransform AngleX="-2.969"/>
                    <RotateTransform/>
                    <TranslateTransform X="-4.567"/>
                </TransformGroup>
            </Button.RenderTransform>
        </Button>
        <Border x:Name="RecBlock" BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Left" Height="56" Margin="1332,32,0,0" VerticalAlignment="Top" Width="122" CornerRadius="30" Background="#FFFB4646" RenderTransformOrigin="-6.375,10.312">
            <TextBlock TextWrapping="Wrap" Text="REC" Margin="19,11" TextAlignment="Center" Foreground="White" FontSize="24"/>
        </Border>
        <Label x:Name="RecTimer" Content="00:00" HorizontalAlignment="Left" Margin="1358,109,0,0" VerticalAlignment="Top" Height="40" Width="76" FontSize="24" Foreground="White"/>
    </Grid>
</Window>

■指定したエリアに限定した録画方法

指定したエリアに限定して録画をするコードはこちらになります。
ここで、screenBmpはウィンドウ全体のBitmapイメージであり、rectが自身で指定したRectangleのエリアになります。

        private bool WriteVideo(bool isStartRec, Bitmap screenBmp, RECT rect)
        {
            m_recordData = rect;

            int capWidth = m_recordData.right - m_recordData.left;
            int capHeight = m_recordData.bottom - m_recordData.top;
         
            if (capHeight <= 0 || capWidth <= 0)
            {
                Console.WriteLine(" size Error");
                return false;
            }
            System.Drawing.Rectangle rectBuf = new System.Drawing.Rectangle(rect.left, rect.top,
                        capWidth, capHeight);
            Bitmap bmp = screenBmp.Clone(rectBuf, screenBmp.PixelFormat);
            Mat mat = BitmapConverter.ToMat(bmp).CvtColor(ColorConversionCodes.RGB2BGR);
            if (isStartRec)
            {
                Cv2.CvtColor(mat, mat, ColorConversionCodes.BGR2RGB);
                Cv2.Resize(mat, mat, new OpenCvSharp.Size(capWidth, capHeight));
                writer.Write(mat);
            }
            return true;
        }

実施していることですが、

System.Drawing.Rectangle rectBuf = new System.Drawing.Rectangle(rect.left, rect.top,
                        capWidth, capHeight);

で切り出し領域のRectangleデータを作成し、

Bitmap bmp = screenBmp.Clone(rectBuf, screenBmp.PixelFormat);

にてウィンドウ全体から切り出すために作成したRectangleで切り出しを行い、
最後はMat型の変数に変換しOpenCVのWrite関数でデータ書き込みを実施しております。

ウィンドウの切り出しですが、下記のようなことを実施しているイメージです。

■指定したエリアの切り出し原理

先ほど記載した通り、指定した座標やRectangleのサイズを指定すればBitmapイメージの切り出しが可能です。
では、座標やRectangleのサイズはCanvas上でマウスで指定したエリアを利用すればよいのでしょうか?

答えはNoです。
Canvasのサイズと実際のウィンドウサイズは異なりますので、座標やRectangleのサイズをそのまま利用するとおかしな位置が切り出されることになります。

このため、
Canvasのサイズと実際のウィンドウサイズの比率で座標やRectangleのサイズを変更させる必要があります。

指定エリアの比率変更のコードはこちらになります。

        public bool DrawRectangle(System.Windows.Point point, double canvasWidth, double canvasHeight,
            ref Position position, ref System.Windows.Shapes.Rectangle rectangle)
        {

            bool ret = false;

            rectangle.Stroke = new SolidColorBrush(Colors.Red);
            rectangle.StrokeThickness = 1;

            var width = Math.Abs(InitPos.X - point.X);
            var height = Math.Abs(InitPos.Y - point.Y);
            rectangle.Width = width;
            rectangle.Height = height;

            if (point.X > canvasWidth - 1)
            {
                width = canvasWidth - InitPos.X;
                Canvas.SetLeft(rectangle, InitPos.X);
                rectangle.Width = width;
                position.left = (int)(InitPos.X);
            }
            else if (point.X < 0)
            {
                width = InitPos.X;
                Canvas.SetLeft(rectangle, 0);
                rectangle.Width = width;
                position.left = 0;
            }
            else if (InitPos.X < point.X)
            {
                Canvas.SetLeft(rectangle, InitPos.X);
                position.left = (int)(InitPos.X);
            }
            else
            {
                Canvas.SetLeft(rectangle, point.X);
                position.left = (int)(point.X);
            }

            if (point.Y > canvasHeight - 1)
            {
                height = canvasHeight - InitPos.Y;
                Canvas.SetTop(rectangle, InitPos.Y);
                rectangle.Height = height;
                position.top = (int)(InitPos.Y);
            }
            else if (point.Y < 0)
            {
                height = InitPos.Y;
                Canvas.SetTop(rectangle, 0);
                rectangle.Height = height;
                position.top = 0;
            }
            else if (InitPos.Y < point.Y)
            {
                Canvas.SetTop(rectangle, InitPos.Y);
                position.top = (int)(InitPos.Y);
            }
            else
            {
                Canvas.SetTop(rectangle, point.Y);
                position.top = (int)(point.Y);
            }

            position.width = (int)(width * (SystemParameters.PrimaryScreenWidth / canvasWidth));
            position.height = (int)(height * (SystemParameters.PrimaryScreenHeight / canvasHeight));
            position.top = (int)(position.top * (SystemParameters.PrimaryScreenHeight / canvasHeight));
            position.left = (int)(position.left * (SystemParameters.PrimaryScreenWidth / canvasWidth));

            return ret;
        }
    }

一部Rectangleの描画が入っておりますが、こちらの解説は
elsammit-beginnerblg.hatenablog.com
を参考にしてください。

比率の変更コードですが、

position.width = (int)(width * (SystemParameters.PrimaryScreenWidth / canvasWidth));
position.height = (int)(height * (SystemParameters.PrimaryScreenHeight / canvasHeight));
position.top = (int)(position.top * (SystemParameters.PrimaryScreenHeight / canvasHeight));
position.left = (int)(position.left * (SystemParameters.PrimaryScreenWidth / canvasWidth));

になります。
実施していることは、
実際のウィンドウとCanvasのサイズの比率を計算の上、比率でCanvas上に作成したRectangleのサイズを拡大・縮小しております。

■最後に

これで自分が録画したいエリアを指定することが出来るようになりました。
後でこちらのアプリはリリースしてみようかと思います。
まずは自分で使ってみて、使い勝手確認していきます。



CreateBitmapSourceFromHBitmapでメモリリークが発生した件

先日よりC#で画面スクリーンショット方法をまとめていました。
elsammit-beginnerblg.hatenablog.com
elsammit-beginnerblg.hatenablog.com


そこで、CreateBitmapSourceFromHBitmap関数でどうもメモリリークが発生してしまい、
メモリ使用量がどんどん増えていく。。

ということで原因と対応策を調べたので備忘録としてまとめておくことしました。
※同じミスはしたくないので。



■原因

メモリリークが発生していたコードですが、こちらのような実装をしておりました。

using (var bmtpScreen = new System.Drawing.Bitmap(
    (int)SystemParameters.PrimaryScreenWidth,
    (int)SystemParameters.PrimaryScreenHeight,
    System.Drawing.Imaging.PixelFormat.Format32bppArgb))
{
    using (var bmpFromImg = Graphics.FromImage(bmtpScreen))
    {
        bmpFromImg.CopyFromScreen(0, 0, 0, 0, bmtpScreen.Size);
        
        Imaging.CreateBitmapSourceFromHBitmap(
            bmtpScreen.GetHbitmap(),
            IntPtr.Zero,
            Int32Rect.Empty,
            BitmapSizeOptions.FromEmptyOptions());
    }
}

こちらのコードですと、
CreateBitmapSourceFromHBitmap実行を繰り返す度にメモリリークが発生してしまい、
困ったことになりました。

そもそもCreateBitmapSourceFromHBitmap関数の引数ですが、
 ・第1引数:アンマネージ ビットマップへのポインター(IntPtr)
 ・第2引数:ビットマップのパレットマップへのポインター(IntPtr)
 ・第3引数:ソースイメージのサイズ(Int32Rect)
 ・第4引数:変換を処理する方法を指定する列挙体の値(BitmapSizeOptions)
となります。
※参考:https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.interop.imaging.createbitmapsourcefromhbitmap?view=windowsdesktop-6.0

自分で作成したコードですが、

return Imaging.CreateBitmapSourceFromHBitmap(
                        bmtpScreen.GetHbitmap(),
                        IntPtr.Zero,
                        Int32Rect.Empty,
                        BitmapSizeOptions.FromEmptyOptions());

でしたので、第1引数のbmtpScreen.GetHbitmap()が怪しそう。。

ということで調べてみたところ、
第1引数に与えている bmtpScreen.GetHbitmap()は明示的に開放しないとメモリリークにつながることを発見。
fkmt5.hatenadiary.org

自分のコードだとメモリ開放はしていないので、
メモリが開放されず蓄積してしまいメモリリークにつながってしまっていたようです。

■解決方法

bmtpScreen.GetHbitmap()のメモリを解放すればよいことが分かりましたので、実際に開放処理を追加すればOK。
開放にはDeleteObject()を利用することにしました。

DeleteObject()とは、、、
WIn32 APIの1つで、
論理ペン、ブラシ、フォント、ビットマップ、領域、またはパレットを削除し、オブジェクトに関連付けられているすべてのシステムリソースを解放
する関数になります。
オブジェクトが削除されると、指定されたハンドルは無効になります。
※参考:https://docs.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-deleteobject

DeleteObject()を利用する場合にはあらかじめ、

        [System.Runtime.InteropServices.DllImport("gdi32.dll")]
        public static extern bool DeleteObject(IntPtr hObject);

と定義すればOK。

後は、

using (var bmtpScreen = new System.Drawing.Bitmap(
    (int)SystemParameters.PrimaryScreenWidth,
    (int)SystemParameters.PrimaryScreenHeight,
    System.Drawing.Imaging.PixelFormat.Format32bppArgb))
{
    using (var bmpFromImg = Graphics.FromImage(bmtpScreen))
    {
        bmpFromImg.CopyFromScreen(0, 0, 0, 0, bmtpScreen.Size);
        
        var ptrBuf = bmtpScreen.GetHbitmap();

        Imaging.CreateBitmapSourceFromHBitmap(
            ptrBuf ,
            IntPtr.Zero,
            Int32Rect.Empty,
            BitmapSizeOptions.FromEmptyOptions());
      
            DeleteObject(ptrBuf);
    }
}

といったように、
DeleteObjectで、

var ptrBuf = bmtpScreen.GetHbitmap()

で定義したリソースを明示的に開放してあげればOKです。

■最後に

今回は自分が躓いたメモリリークに関してまとめておきました。
次回同じようなコードを書く時には気を付けたいと思います。



WPFで自由にRectangleを描画する

先日、WPFで特定のアプリケーションに限定してスリーンショット録画の方法をまとめました。
elsammit-beginnerblg.hatenablog.com

ですが、
 ・複数アプリを録画したい
 ・好きなエリアを指定したい
といった場合にはアプリケーションに限定するだけでは不十分です。。
そこで、自分で指定したエリアで録画するアプリケーションを実装したいと考えました!

今回は、エリアを指定するためにRectangleを描画する方法をまとめたいと思います。



xamlの実装

今回メインウィンドウとして実装したxamlはこちらになります。
ウィンドウにBorderで枠線を作成し、その中にCanvasを差し込んでいるシンプルな構成です。
Canvasの名前は"CanvasArea"としました。

Canvas上ではトリガとしてこちらを定義しました。
 ・マウスの左ボタン押す :MouseLeftDwn
 ・マウスの左ボタン上げる:MouseLeftUp
 ・マウスを動かす    :MouseMoving

<Window x:Class="WpfApp2.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:WpfApp2"
        mc:Ignorable="d"
        Title="MainWindow" Height="408.739" Width="659.456">
    <Grid>
        <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Left" Height="277" Margin="64,49,0,0" VerticalAlignment="Top" Width="528" Background="#00000000">
            <Canvas x:Name="CanvasArea" HorizontalAlignment="Left" Height="273" Margin="0,1,0,0" VerticalAlignment="Top" Width="523" Panel.ZIndex="1" PreviewMouseLeftButtonUp="MouseLeftUp" PreviewMouseLeftButtonDown="MouseLeftDwn" PreviewMouseMove="MouseMoving" Background="White" />
        </Border>
    </Grid>
</Window>

■好きなエリアにRectangleを作成する

では早速、Canvas上にエリアを指定するためのRectangleを描画してみたいと思います。
コードはこちら。

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

namespace WpfApp2
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        bool isWriting = false;
        Point Init;
        private List<UIElement> RectangleList = new List<UIElement>();
        UIElement RectElement = new UIElement();

        public MainWindow()
        {
            InitializeComponent();
        }

        private void MouseLeftDwn(object sender, MouseButtonEventArgs e)
        {
            Console.WriteLine("Click Mouse Dwn");
            Canvas c = sender as Canvas;
            Init = e.GetPosition(c);
            c.CaptureMouse();
            isWriting = true;
        }

        private void MouseLeftUp(object sender, MouseButtonEventArgs e)
        {
            
            if (isWriting)
            {
                Console.WriteLine("Click Mouse Up");
                Canvas c = sender as Canvas;
                isWriting = false;
                c.ReleaseMouseCapture();
            }
        }

        private void WriteRectangle(Point point)
        {
           CanvasArea.Children.Remove(RectElement);
           

            Rectangle rect = new Rectangle();
            rect.Stroke = new SolidColorBrush(Colors.Red);
            rect.StrokeThickness = 1;

            rect.Width = Math.Abs(Init.X - point.X);
            rect.Height = Math.Abs(Init.Y - point.Y);

            if(Init.X < point.X)
            {
                Canvas.SetLeft(rect, Init.X);
            }
            else
            {
                Canvas.SetLeft(rect, point.X);
            }

            if(Init.Y < point.Y)
            {
                Canvas.SetTop(rect, Init.Y);
            }
            else
            {
                Canvas.SetTop(rect, point.Y);
            }

            CanvasArea.Children.Add(rect);
            RectElement = rect;
        }

        private void MouseMoving(object sender, MouseEventArgs e)
        {
            if (isWriting)
            {
                Console.WriteLine("Click Mouse Move");
                Point pos = e.GetPosition(CanvasArea);
                WriteRectangle(pos);
            }
        }
    }
}

マウスの左ボタン押すとMouseLeftDwnが実行されます。
MouseLeftDwnでは、
 ・マウスの強制キャプチャ
 ・マウスの位置を記憶
 ・マウスを押下したフラグをtrue
します。

        private void MouseLeftDwn(object sender, MouseButtonEventArgs e)
        {
            Console.WriteLine("Click Mouse Dwn");
            Canvas c = sender as Canvas;
            Init = e.GetPosition(c);
            c.CaptureMouse();
            isWriting = true;
        }

マウスの左ボタン押下した状態でマウスを動かすと、
MouseMovingがコールされます。
MouseMovingではWriteRectangleがコールされ、こちらでRectangleを描画します。

        private void MouseMoving(object sender, MouseEventArgs e)
        {
            if (isWriting)
            {
                Console.WriteLine("Click Mouse Move");
                Point pos = e.GetPosition(CanvasArea);
                WriteRectangle(pos);
            }
        }

最後にマウスの左ボタンを離すと、
MouseLeftUpがコールされます。
MouseLeftUpでは、
 ・マウスの強制キャプチャ解除
 ・マウスを押下したフラグをfalse

        private void MouseLeftUp(object sender, MouseButtonEventArgs e)
        {
            
            if (isWriting)
            {
                Console.WriteLine("Click Mouse Up");
                Canvas c = sender as Canvas;
                isWriting = false;
                c.ReleaseMouseCapture();
            }
        }

このコードを実行すると、
こちらのような結果となります。

■Rectangle描画領域をCanvasに限定する

先ほどのコードでRectangleを好きな位置に描画することが出来るのですが、
領域外にRectangleをはみ出して描画できてしまいます。

次に、Canvas領域内に限定してRectangleを留めるようなコードを実装してみます。
変更するのは、WriteRectangleのみになります。
コードはこちら。

        private void WriteRectangle(Point point)
        {
           CanvasArea.Children.Remove(RectElement);
           

            Rectangle rect = new Rectangle();
            rect.Stroke = new SolidColorBrush(Colors.Red);
            rect.StrokeThickness = 1;

            rect.Width = Math.Abs(Init.X - point.X);
            rect.Height = Math.Abs(Init.Y - point.Y);

            if (point.X > CanvasArea.ActualWidth)
            {
                Canvas.SetLeft(rect, Init.X);
                rect.Width = CanvasArea.ActualWidth - Init.X;
            }
            else if (point.X < 0)
            {
                Canvas.SetLeft(rect, 0);
                rect.Width = Init.X;
            }
            else if (Init.X < point.X)
            {
                Canvas.SetLeft(rect, Init.X);
            }
            else
            {
                Canvas.SetLeft(rect, point.X);
            }

            if (point.Y > CanvasArea.ActualHeight)
            {
                Canvas.SetTop(rect, Init.Y);
                rect.Height = CanvasArea.ActualHeight - Init.Y;
            }
            else if (point.Y < 0)
            {
                Canvas.SetTop(rect, 0);
                rect.Height = Init.Y;
            }
            else if (Init.Y < point.Y)
            {
                Canvas.SetTop(rect, Init.Y);
            }
            else
            {
                Canvas.SetTop(rect, point.Y);
            }

            CanvasArea.Children.Add(rect);
            RectElement = rect;
        }


実施していることですが、Canvasのエリア外に出てしまった場合にはサイズを
Canvasサイズ - 初期座標位置
とすることで、Canvasサイズ外にRectangleがはみ出さないように実装しております。

実際に動かすとこちらのようになります。

■最後に

今回はWPFで自由に位置やサイズを指定してRectangleを描画するコードをまとめました。
これをスクリーン録画とマージしてエリア指定して録画するアプリにしていきたいと思います!!



特定のアプリに限定したスクリーン録画を試みる

先日、OpenCVSharpを用いて画面全体でのスクリーン録画を試してみました。
elsammit-beginnerblg.hatenablog.com

ですがこの方法ですと、
特定のアプリが映っている領域のみ録画したい場合には無駄な領域も写ってしまいますね。
後で余計な領域を削除するのも大変なので、今回は特定のアプリケーションの領域のみを録画する方法をまとめたいと思います!!



■前提条件

アプリケーション領域に限定したスクリーン録画の方法をまとめるにあたり、
前回の画面全体のスクリーン録画で用いたコードを流用します。

このため本ブログではxamlコードの掲載は割愛します。
必要に応じて前回のブログをご参照ください。

■アプリケーションの座標やサイズ取得方法

アプリケーションの領域に限定したスクリーン録画をまとめる前に、
アプリケーション領域を特定するための座標やサイズを取得する方法をまとめます。

アプリケーションの座標やサイズ取得するにあたり、
Windows APIの1つであるuser32.dllを用います。
user32.dllを用いるにあたり、

using System.Runtime.InteropServices;

を定義ください。

さらに、user32.dllのGetWindowRect()APIを用いるためあらかじめ、
こちらのコードも定義しておきます。

        [DllImport("user32.dll")]
        private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);

        [StructLayout(LayoutKind.Sequential)]
        private struct RECT
        {
            public int left;
            public int top;
            public int right;
            public int bottom;
        }

GetWindowRectは該当するウィンドウハンドルに対する座標情報を返すAPIになります。
引数ですが、
 ・IntPtr hwnd:ウィンドウハンドル
 ・RECT lpRect:ウィンドウハンドルに対する座標情報
になります。

ここまでがアプリケーションの座標や領域の情報を取得するための前準備。
では実際に情報を取得する関数を作成していきます。
コードはこちら。

private bool GetAppPosition(ref RECT rect)
{
    bool flag = false;
    string appName = "notepad"; //ここに実行ファイル名(拡張子なし)を記入する.

    try
    {
        var mainWindowHandle = System.Diagnostics.Process.GetProcessesByName(appName)[0].MainWindowHandle;
        flag = GetWindowRect(mainWindowHandle, out rect);
    }
    catch
    {
        flag = false;
    }

    return flag;
}

appName変数に記入したアプリケーション名(実行ファイル名)の座標を取得するコードになります。
さらに、アプリケーション名が存在しない場合や領域取得できなかった場合にエラーとするためにOK/NGを示すflagを返すようにしております。

System.Diagnostics.Process.GetProcessesByName(appName)は、
引数に与えたアプリケーション名を元に全てのプロセス情報を返すAPIになっております。
今回は取得できたプロセス情報のうち1つ目に見つけられたプロセス情報のみを抽出した上で、
プロセス情報のうち、ウィンドウハンドル情報をmainWindowHandle 変数に代入しています。

var mainWindowHandle = System.Diagnostics.Process.GetProcessesByName(appName)[0].MainWindowHandle;

後は先ほど用意した、
GetWindowRect(mainWindowHandle, out rect);
でウィンドウハンドルから座標情報を取得して終了です。

■特定アプリの領域を切り出してスクリーン録画してみる

では本題の特定アプリの領域を切り出してスクリーン録画を行う方法をまとめます。
といっても座標情報を先ほどのコードで得られるので後は領域指定して録画するのみになります。

全体のコードはこちら。

using System;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Drawing;
using System.Windows.Interop;
using System.Threading;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using System.Runtime.InteropServices;

namespace WpfApp1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : System.Windows.Window
    {
        [DllImport("user32.dll")]
        private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);

        [StructLayout(LayoutKind.Sequential)]
        private struct RECT
        {
            public int left;
            public int top;
            public int right;
            public int bottom;
        }


        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Thread thread = new Thread(new ThreadStart(() =>
            {
                CaptureMovieAsync();
            }));
            thread.Start();
        }

        private void CaptureMovieAsync()
        {
            using (var writer = new VideoWriter("test.wmv", FourCC.WMV3, 5, new OpenCvSharp.Size((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight)))
            {


                for (int i = 0; i < 100; i++)
                {
                    RECT rect = new RECT();
                    bool flag = GetAppPosition(ref rect);
                    int width = rect.right - rect.left;
                    int height = rect.bottom - rect.top;

                    using (var screenBmp = new System.Drawing.Bitmap(
                        width,
                        height,
                        System.Drawing.Imaging.PixelFormat.Format32bppArgb))
                    {
                        using (var bmpGraphics = Graphics.FromImage(screenBmp))
                        {
                            bmpGraphics.CopyFromScreen(rect.left, rect.top, 0, 0, screenBmp.Size);

                            Dispatcher.Invoke((Action)(() =>
                            {
                                ImgCap.Source = Imaging.CreateBitmapSourceFromHBitmap(
                                screenBmp.GetHbitmap(),
                                IntPtr.Zero,
                                Int32Rect.Empty,
                                BitmapSizeOptions.FromEmptyOptions());

                                Mat mat = BitmapConverter.ToMat(screenBmp).CvtColor(ColorConversionCodes.RGB2BGR);
                                Cv2.CvtColor(mat, mat, ColorConversionCodes.BGR2RGB);
                                Cv2.Resize(mat, mat, new OpenCvSharp.Size((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight));
                                writer.Write(mat);
                            }));
                        }
                    }
                    Thread.Sleep(100);
                }
            }    
        }

        private bool GetAppPosition(ref RECT rect)
        {
            bool flag = false;
            string appName = "notepad"; //ここに実行ファイル名(拡張子なし)を記入する.

            try
            {
                var mainWindowHandle = System.Diagnostics.Process.GetProcessesByName(appName)[0].MainWindowHandle;
                flag = GetWindowRect(mainWindowHandle, out rect);
            }
            catch
            {
                flag = false;
            }

            return flag;
        }
    }
}

追加・変更したコードはこちらのみ。

 for (int i = 0; i < 100; i++)
                {
                    RECT rect = new RECT();
                    bool flag = GetAppPosition(ref rect);
                    int width = rect.right - rect.left;
                    int height = rect.bottom - rect.top;

                    using (var screenBmp = new System.Drawing.Bitmap(
                        width,
                        height,
                        System.Drawing.Imaging.PixelFormat.Format32bppArgb))
                    {
                           using (var bmpGraphics = Graphics.FromImage(screenBmp))
                        {
                            bmpGraphics.CopyFromScreen(rect.left, rect.top, 0, 0, screenBmp.Size);

実施していることは、
GetAppPosition()関数にて取得できたRECT情報からアプリケーションのサイズを算出。
算出した結果をscreenBmp定義時にサイズ情報として入力かつ、
CopyFromScreen()関数にて取得するスクリーンショットの領域をアプリケーションの領域に指定しているのみになります。

アプリケーションの座標さえ手に入ってしまえば後はスクリーンショットとして取得する領域を合わせるだけですね。

■最後に

今回は特定のアプリケーションに限定したスクリーン録画を試してみました。
最近スクリーンショット取得すること多いので利用していきたいと思います。