高見知英の技術ログ

技術関係のログをQiitaから移行してきました。プログラミングのほか、使っているアプリの細かい仕様についてなど書いていきます。

Lazarusでテストを書く

このブログはLazarus(FreePascal)アドベントカレンダー5日目の記事です。

qiita.com

5日目はおまちかね(?)テストについて。

コンパイルして動かすLazarusのプログラムはとくに、メソッドを作ったときのテストがないのは不安です。

なのでとりあえず、Lazarusでテストプロジェクトをつくって、それを実行してみます。

Lazarus(というかFreePascal)のテストフレームワーク:FPCUnit

FreePascalのテストフレームワークとしては、FPCUnitというものがあります。

LazarusにはこのFPCUnitを使ったテストプロジェクトを作成するためのテンプレートが装備されているため、これを使うのが便利です。

f:id:TakamiChie:20211203182319p:plain
テストプロジェクトの作成

ファイルメニューの「新規…」より、FPCUnit Test Applicationを選択します。

なお、GUIを操作するのは地味に面倒くさいのでFPCUnit Console Test Applicationを使えば良いじゃん という風には思ったのですが、ちょっと使ってみた感じ使い方がよくわかりませんでした…。

そのため、今回はとりあえずGUIのあるほうを選びます。

プロジェクトグループを作る

上記の通りですが、Lazarusのテストはテストプロジェクトという形で作成していきます。

アプリ開発の最中にメインのプロジェクトとテストプロジェクトを行ったり来たりするのは面倒なので、プロジェクトグループを作っておくと良いでしょう。

プロジェクトグループの作り方については、二日目の記事にも書いたので、そちらを参照。

devblog.onpu-tamago.net

メインプロジェクトとは別に、テストプロジェクトを作り、テスト対象となるユニットファイルをプロジェクトエクスプローラの「追加」ボタンより追加していきます。

f:id:TakamiChie:20211203182232p:plain
プロジェクトエクスプローラ

Lazarusはユニットファイルが構築パスに入っていればどこに置いていてもファイル名で参照できるので、ファイルの置き場は特に気にする必要はありません。

テストユニットを作る

テストユニットファイルは、同じく新規ダイアログより、FPCUnit Test Caseを使います。

f:id:TakamiChie:20211203182855p:plain
FPC Unit Test Case

テストケース名と'Setup'メソッド等を作成するかどうか聞かれますので、適当な値を設定してOK。

テストのやり方は、ほかの一般的な○○○Unitと同様、処理対象のメソッドを呼んだあと、その戻り値を期待値とAssertEqualsメソッドで照合します。

procedure TScriptProcessTestCase.TestLaunchProgram;
var
  Proc : TScriptProcess;
  StdOut, StdErr: String;
begin
  Proc := TScriptProcess.Create();
  try
    Proc.Execute('scripts\\SPTEST_TestScript.py', StdOut, StdErr);
    AssertEquals(StdOut, 'Hello This Is a TEST' + #13#10);
  finally
    Proc.Free;
  end;
end;

使用したクラスの解放は… まあ、忘れたところでどうせテストなのでそんなに問題になることもないかと思いますが、いちおうクセをつけておくと良いかと。

いざ実行

いざいくつかテストを作って実行すると次のようなアプリが起動します。

f:id:TakamiChie:20211203183906p:plain
テストアプリ実行環境

使い方に迷うことはまあないと思いますが、ツールバーの▶ボタンを押すとテストが実行されます。結果はXMLで出力することもできるようです。

スクリプトファイルの読み込みやリソースなどは、なるべく本プロジェクトと共有しない

またここからは一般的なテスト作成の方針になるとは思いますが、プログラムの中でスクリプトファイルの読み込みやリソースファイルを使用する際は、本プロジェクトとは別のものを使うと良いでしょう。

あとあと本プロジェクトの側で修正が必要になった場合、テストの書き換えが必要になる場合があります。

面倒でも同じようなリソースファイルをテスト用に作っておくと良いでしょう。

Lazarusで別のプロセスを呼び出す

このブログはLazarus(FreePascal)アドベントカレンダー4日目の記事です。

qiita.com

Lazarusでほかのプロセスを呼び出す場合、ProcessモジュールにあるTProcessというクラスを使用します。

使い方は大まかには上記サイトに記載の通りですが、TProcess.Createメソッドでオブジェクトを作成、TProcess.Executableプロパティに実行したいファイルおよびコマンドの名前を、TProcess.ParametersプロパティにAddメソッドを使って引数を追加し、TProcess.Executeメソッドで実行します。開いたプロセスの終了を待つ場合はTProcess.WaitOnExit();メソッドを呼び出して終了待ちを行ないます。

詳細は以下をご覧ください。

github.com

Lazarusでリソースを使う

このブログはLazarus(FreePascal)アドベントカレンダー3日目の記事です。

qiita.com

Lazarusでリソースを使う画像やアイコン、スクリプトファイルなどのファイルはリソースとしてプロジェクトに取り込んでおくと便利です。

Lazarusで扱えるリソースには2種類があります。

  • resファイル:プロジェクトオプションから追加できるリソース形式。TResourceStreamオブジェクトで読み込みが可能。
  • lrsファイル:LazResというユーティリティファイルによって生成するリソース形式。LazarusResourcesというTLResourceList型のオブジェクトにより取得が可能。

どちらもリソースの名前を指定することができるという点では同じですが、resファイル形式はn番目のリソース取得したり、リソースの名前一覧を取得することができない(っぽい)のに対し、lrsファイルはリソースの個数を取得したり、n番目のリソースを取得するなどといったことができます。

res形式 lrs形式
作成方法 プロジェクトオプションより LazResユーティリティより
読み取り方法 TResourceStreamを使う LazarusResourceを使う
クラスの定義ユニット Classes LResources
for文で走査 できない できる

TResourceStreamはTStreamの子オブジェクトなので、画像などのバイナリファイルを基本に、内部のスクリプトファイルなどにはLazarusResourcesを使うと良いでしょう。

LazResをいちいち呼ぶのが面倒くさい

LazResユーティリティはLazarusインストールディレクトリの\tools\lazres.exeにあります。そこで、プロジェクトオプションより起動前にpython .\resourcemaker.py --lazarus_dir $(LazarusDir)などとしてリソースを整理するためのスクリプトを実行しておくと良いでしょう。実例は以下のあたりを参照。

github.com

lrsファイルの中身が古い?

なお、とくにlrs形式の場合、ファイルをせっかく書き換えたのになぜか参照されるリソースファイルが古い ということがあります。

どうもlrs形式の場合、ただ「実行」しただけではlrsが更新されないことがあるようです。

コンパイルまたは構築を実行してみましょう。

LazarusプロジェクトをGit管理する

このブログはLazarus(FreePascal)アドベントカレンダー2日目の記事です。

qiita.com

Lazarusでプロジェクトを作成する場合、まず気になるのがGitで管理できるかということです。

Delphiも昔は「他人が作ったコンポーネントの情報とかを自プロジェクトに含めなくて済むのでソースコードが公開しやすい!」みたいなことを思っていましたが、考えてみればPythonのpipenvみたいなものがないので他者がソースコードを持ってきてビルドする なんていうことがやりづらかった。

Lazarusのほうでもその問題は解決されてない(?)ようですが、とりあえず三者の作ったコンポーネントやクラスライブラリを使っていない場合はGit管理ができます。

要らないファイルはどれか?

さて、Git管理する場合、まず気になるのが管理対象からはずすべきファイルの存在。とりあえずGithubの新規プロジェクト生成時に使用できるPascalの.gitignoreテンプレートを使ったため抜けはあるかもしれませんが、以下のファイルを無視すると、とりあえずLazarusプロジェクトをGit管理できます。

  • 成果物・中間ファイルなど
    • *.exe
    • *.dll
    • *.o
    • lib/
  • リソースファイルなど
    • *.res:一見リソースファイルなので管理必要なのですが、リソースをまとめて実行ファイルに込める直前の中間ファイル的な存在のようなので、管理不要です
    • *.lrs:Lazarusリソースファイル。これが管理不要かどうかはシチュエーションに寄ります(うちはresourcemaker.pyで自動生成するようにしてしまったので管理不要になりました)
  • 設定ファイル
    • *.lps:Lazarusプロジェクトの設定ファイル(どのファイルをエディタで開いているか、どの行を選択しているかなど)
    • *.fpcunit.ini:Lazarusテストプロジェクトの設定ファイル
  • その他
    • backup/

というわけで、これらを無視ファイル設定すればいけそうです。

お勧めのディレクトリ構成

お勧めのディレクトリ構成は・・・といっても、Lazarusのプロジェクトエクスプローラは機能がそれほど多くなく、Lazarus自体同一プロジェクト内のファイルはuses句に名前を追記してあげればそれだけで呼べるようになるため

  • 役割ごとに別々のフォルダを作ってソースファイルを置く
  • それらをまとめてLazarusのプロジェクトエクスプローラに追加する

くらいで良さそうです。あとは今後書く予定のテストについては「テストプロジェクト」という扱いになるので、テストプロジェクトを作成する予定であれば以下のような感じがよさそう

プロジェクトルート
  |--src
  |    `--実際に作るプログラムのソースコード
  `--tests
      `--テストプロジェクトのソースコード

プロジェクトグループ

前述テストプロジェクトなんかもありますが、近いプロジェクトを別管理するのも面倒です。

そんなときのために、Lazarusには「プロジェクトグループ」という機能があります(たぶんVisual Studioなどでいうソリューション相当)。

あ、メニュー項目名は日本語化してる前提です。

  • 公式Wikiの内容を参考にProject Groupsの機能を有効にする。
  • メインメニュー>パッケージ>パッケージをインストールもしくはアンインストール
  • 右側の「インストール可能パッケージ」の一覧より「lazprojectgroups」を選択して「選択対象をインストール」をクリック
  • 「保存してIDEを再構築」をクリック
  • 確認ダイアログが出るのでそのままOK

しばらくIDEが再構築されたあと自動的に再起動されるので、プロジェクトメニューより「New Project group...」をクリックして使います。

f:id:TakamiChie:20211129094454p:plain
Project group

やってることはプロジェクト名をクリックするとプロジェクトを開き直すだけなんですが、意外と使いやすいので有効にしておくと良いです。

肝心のGit管理機能は・・・

さて、肝心のLazarusのGit管理機能は・・・というと、残念ながらLazarusはGitに対応しておりません。

個人的には外部ツールにVisual Studio Codeを追加してそれを使ってます(Github Desktopなどでもいいでしょうが細かい編集などがしづらいので)。外部ツールの設定でVisual Studio Codeなどを指定しておくと良さそう(プロジェクトグループを作成しているとこの書き方じゃダメかも)。

f:id:TakamiChie:20211129095354p:plain
ツールの追加

というわけでLazarusでもソースコードをGit管理していきましょう。

Lazarusってなんぞや

このブログはLazarus(FreePascal)アドベントカレンダー1日目の記事です。

qiita.com

さて、今回は以前もちょこっと話題にしたLazarusという環境について2,3書いてみようと思い、勢いでこのカレンダーを立ち上げました(DelphiアドベントカレンダーはあるけどDelphi言語を使うわけではないからDelphiのほうではないですしね…)

Lazarusとは

Lazarusとは、現在Embarcaderoが販売しているDelphiという開発環境で扱う、Delphiという言語の元になった言語*1Pascalのもう一つの潮流、FreePascal用の開発環境です。

昔のDelphiに割とよく似た構成をしており、Delphiを過去使ったことがある人はまあ迷うことなく使えるでしょう。

f:id:TakamiChie:20211129084007p:plain
Lazarusスクリーンショット

言語としてもだいぶDelphiと似通っており、とりあえずこれまたDelphiを使ったことがある人ならばそこまで悩むことなく使えると思います。

Delphi6とかそのへんとおなじ水準です。割と古い記法が使えます。無名関数もジェネリクス(配列などの型指定)もできないし、GCもありません。ただまあ、LCL(DelphiVCLみたいなもの)がまともに使えるのでまあまあ使える といった感じ。

YouはどうしてLazarusを?

なんでまたLazarusなんてマイナーな環境を使ったか というと、だいたい以下のような理由で

  • PCで常駐型のアプリケーションを作ろうと思ったため(PythonでもGUIは作れるがさすがにそれだと起動に時間がかかりすぎる)
  • C#などだとVisual Studioありきになってしまうが、SSD容量の問題でVisual Studioが入らないのと、以前Visual Studioを使ったときにアップデート地獄*2に悩まされたことからあまり使いたくない
  • かといって本家DelphiのCommunity Editionは初期のビルド対象が今更Win32で、さすがにそれはないだろと思ったこと(とはいえLazarusが64ビット対応しているかというとそういうわけではないかも)
  • 昔ながらのDelphiっぽい環境で開発を行ないたい気分も少しあった

ということで、とりあえずこれを使ってアプリを作ってみよう と思い立ったわけです。

Lazarusのインストール

とりあえずLazarusをインストールするには、Lazarusのサイトより使っているOSのパッケージをダウンロード

www.lazarus-ide.org

Chocolateyをお使いの場合choco install -y lazarusでもいけます。最近wingetでもインストールできるらしい。

Lazarus環境の日本語化

UI部分の一部しか日本語化できないため、別に日本語化と言えるほどカッコいいものではありませんが、Lazarusの環境は設定より日本語化できます。

  • メインメニュー>Tools>Options
  • 設定の検索で「Language」と入力
  • Environment>Generalを選択
  • 右ペインのLanguageコンボボックスからjaを選択
  • 「次に表示されるオブジェクト」から日本語化されるようなので、Files>RestartでIDEを再起動

f:id:TakamiChie:20211129093135p:plain
Lazarusの日本語設定

作ったものはこんな感じ

とりあえず作ったものはこんな感じです。

github.com

いけそうなときは0000Studioで開発作業を配信していますので、覗いていただけると幸いです。

*1:たしかBorlandが開発していた当初、「これはObjectPascalではなくDelphi言語だ」みたいなことを書いている記事があった気がするのでそう書く

*2:起動する度にIDEのなんらかのアップデートにより作業が止まる

PowerShellの文字化け対策

PowerShellのバージョンを5.1.19041.1320にあげてから、急に既存のスクリプトが文字化けを起こすようになってしまいました*1

とりあえず「PowerShell 文字化け」などで検索すると「-NoExit -Command "chcp 65001"という文言をPowerShellの起動ショートカットに加えると良い」という結果が出てきますが、これだとすべてのPowerShell起動シチュエーション(コンテキストメニューにある「PowerShellで実行」など)にこのコマンドを追加してあげなければならず、面倒です。

Microsoft.PowerShell_profile.ps1を編集する

そんなときのために、PowerShellには常にコンソール起動時に必ず実行されるスクリプトとして、Microsoft.PowerShell_profile.ps1というファイルがありますので、これを使います。

%USERPROFILE%\Documents\WindowsPowerShell\というフォルダ(なければ作成)に、Microsoft.PowerShell_profile.ps1というファイルを作って、以下の行を付け加えます。

chcp 65001

これで、PowerShellを起動したときに必ずこのコマンドが実行されるようになりますので、各種ショートカットの編集などは不要になります。

なぜchcp 65001

chcpコマンドは、コンソールの文字コードを変更するためのコマンドです。

最近のWindowsであれば基本文字コードUnicodeなのでわざわざ文字コードを変更する必要なんてないはず・・・ と思いきや、日本語環境の場合、PowerShellShift_JISになってしまうようです。なんてこった。

このため、明示的に文字コードを65001(UTF-8)に設定する必要があるとのことでした。

コンソール上の日本語が読めなくなった場合

なおこの操作をすると、コンソール上のにほんごがすべて豆腐になってしまい、よめなくなることがあります。

f:id:TakamiChie:20211130222839p:plain
豆腐 襲来

これはPowerShellUTF-8モードのフォントが欧文フォントなため、日本語が表示されないことによるものらしい。

PowerShellのタイトルバーを右クリックで表示される「プロパティ」より、日本語フォントを指定すると、ちゃんと日本語が表示されるようになります。

f:id:TakamiChie:20211130222955p:plain
PowerShellコンテキストメニュー

個人的にはBIZ UDゴシックあたりが読みやすくてお勧め。

それでも文字化けが発生する場合…

それでもps1ファイルを実行しようとした場合など、文字化けが発生する場合、ps1ファイルの文字コードを確認する必要があります。

PowerShellが解釈できるps1ファイルは、「UTF-8(BOMあり)」なようですので、ps1ファイルの文字コードがほかのものになってないか確認しましょう。

参考資料

*1:PowerShellの最新バージョンは7.xだろ!というツッコミもあるかもしれませんが、未だWindows 10のエクスプローラのリボンメニューからPowerShellを起動すると、5.xが起動するのです

tkinterで表示するダイアログをマウスカーソル位置に表示する

Pythonで簡単なGUIを表示する場合、標準モジュールで何も追加しなくても使えるtkinterが便利です。

tkinterには、tkinter.simpledialog.Dialogというダイアログを表示するためのひな形クラスが存在するので、それを使用すると割と簡単にダイアログを作ることが可能です。

# 初期案
import tkinter.simpledialog

class ChooseDialog(tkinter.simpledialog.Dialog):

  def body(self, master) -> None:
    # 入力部分に表示するコントロールを作成する
    return super().body(master)

  def ok(self, event=None):  super().ok(event); self.result = True
  def cancel(self, event=None): super().cancel(event); self.result = False

owner = tkinter.Tk()
owner.withdraw()
dlg = ChooseDialog(owner, 'Choose Link Style')

ダイアログをマウスポインタの位置に表示したい

さて、このダイアログ、初期の設定だとウィンドウの中央に画面が表示されるようになっています*1

ただ、個人的にはちょっとしたダイアログなのでマウスカーソルの位置に表示させて、すぐに操作できるようにしたい。マウスポインタの位置を取得すればできるはずですので、やってみましょう。

# 動かないバージョン
import tkinter.simpledialog, ctypes, ctypes.wintypes

class ChooseDialog(tkinter.simpledialog.Dialog):

  def body(self, master) -> None:
    p = ctypes.wintypes.POINT()
    ctypes.windll.user32.GetCursorPos(ctypes.byref(p))
    
    self.geometry(f"+{p.x - self.winfo_width() // 2}+{p.y - self.winfo_height() // 2}")
    # 入力部分に表示するコントロールを作成する

    return super().body(master)

  def ok(self, event=None):  super().ok(event); self.result = True
  def cancel(self, event=None): super().cancel(event); self.result = False

Pythonからマウスカーソルを取得する方法については、ググってみても大抵「PyAutoGUI.positionを使え」と書いてあるのですが、マウスポインタをとるためだけにPyAutoGUIをインポートするのはちょっと・・・ と思い、PyAutoGUIのソースを参照してみたところ、ふつうにWindows APIGetCursorPosを呼んでるだけだったので、こちらを使いました。

さて、そんな感じでマウスカーソルの周囲にウィンドウを表示するよう、ウィンドウのジオメトリを設定するのですが、なぜか動きません。なぜか。

調べてみると、tkinter.simpledialog.Dialogはコンストラクタで_place_windowというプライベート関数を呼んでいるようで、このメソッドがダイアログの位置を設定しているようです。

それでも強引にウィンドウの位置を設定する

それでも自分でダイアログの表示位置を設定したい場合は、_place_window()関数の呼び出しよりあとにウィンドウの位置を矯正する必要がある。

と、いうことで、引数がなく副作用も最小限で済みそうな、grab_set()メソッドをオーバーライドして、処理を付け足します。

# 動くバージョン
import tkinter.simpledialog, ctypes, ctypes.wintypes

class ChooseDialog(tkinter.simpledialog.Dialog):

  def body(self, master) -> None:
    # 入力部分に表示するコントロールを作成する
    return super().body(master)

  def grab_set(self) -> None:
    p = ctypes.wintypes.POINT()
    ctypes.windll.user32.GetCursorPos(ctypes.byref(p))
    self.geometry(f"+{p.x - self.winfo_width() // 2}+{p.y - self.winfo_height() // 2}")
    return super().grab_set()

  def ok(self, event=None):  super().ok(event); self.result = True
  def cancel(self, event=None): super().cancel(event); self.result = False

こうすると、マウスカーソルの位置にウィンドウを表示させることができます。

わかったこと

  • tkinter.simpledialog.Dialogはちょっとしたダイアログを表示させたいときに結構便利
  • 挙動が分からないことがあったらソースコードを読もう(Visual Studio Codeからであれば、Ctrlキーを押しながらキーワードをクリックでもソースが表示できる)
  • Pythonの標準モジュールのソースはGithubでも公開されているのでそちらも確認を
  • なんだかんだ他人のソースコードを読みたいときに読めるようにしておくトレーニングは重要

参考文献

*1:の、はずなんですが、なぜか自分の環境では1モニタ目の画面左上に表示されます…。マルチスクリーンに対応していないのか・・・?