画像のフェードアウトいろいろ[VCL]

C++Builderで、表示した画像をじわじわ消していくのはFireMonkeyならほぼコードいらずで可能だが、VCLだと自前で処理する必要がある。いろんな方法を試してみたので暫定まとめ。使いどころはよく分からないが。

0.事前準備

TFormにTImageを置く。
ちらつき防止のためFormのDoubleBufferedプロパティをtrueにしておく。

1.ランダムにPixel操作(力業)

画像がぽつぽつと白くなっていく効果。
画像のPixel分の数値配列をシャッフルして、順番に白くしていくだけのシンプルな力業だが、実際にやってみると一手間かけないと自然な効果が出ない。

基本形

まず基本形。
手順は単純で
 ①画素数分の数値配列を作ってシャッフル
 ②シャッフルした数値のPixelをループで白くする
だけ。

多めに変化

上記のコードだと、PCの性能や画像サイズにもよるがかなり遅い。複数のPixelを白くするよう改良してみる。

実際にやると目の錯覚(たぶん)でだんだん遅くなっているように感じる。

変化量を増やしていく

白くするPixel数がどんどん増えるように改良。

だいぶ自然な感じになった。

Bitmap以外の場合

ここまで使ったImage->CanvasはBitmap以外はアクセスできない。
他の画像形式はBitmapに代入して行うのが安直だけど手軽。

まとめコード

以上のまとめコード。

2.色を薄くしていく

画像が段々薄くなるパターン。
RGBをいじるだけ。ScanLineを使えるので、ランダムなピクセル操作よりは高速。

かなりスムースになる。

3.アルファチャンネルを使う

TBitmapのアルファチャンネルを設定し、だんだん薄くしていく手もある。
(画像タイプみて変換する処理は省略。TPNGImageだと別の方法があるらしい)

実行結果。

4.Drawメソッドの第4引数を使う(お手軽)

Canvas->Drawメソッドで、第4引数の不透明度(Opacity)を指定する方法。
これはかなり手軽。TImageの画像がどんな形式でも、変換する必要はない。

C++Builder10.2.3 / Windows10(21H2)で確認。

TImageに表示されている画像の種類を知る[VCL]

C++Builder(Delphi)のTImageにはJpegやPng、ビットマップなど多様な画像が読み込めるが、あとから画像種類を知りたい場合
 Image->Picture->Graphic->ClassName()
で画像のVCLクラス名が取れる。今さら知ったのでメモ。

C++Builder10.2.3 / Windows10(21H2)で確認。

[Vcl]AcrobatDCを利用したPDF印刷が失敗する対策

Adobe Acrobatはクラウド版(Acrobat DC)になってから使っていなかったのだが、アプリケーションの印刷ダイアログで選択して使用すると、PDF作成に失敗するようになっていた。

以下は、四角形を1つ描くだけの単純なテストプログラム。

PrintDialogでAdobe PDFを選択すると、下記のダイアログが出たまま止まる。

Microsoft Print to PDF および JUST PDF4 だと問題なくPDFが作成された。

Acrobat DCは最近のアップデートで、どうやら「文書名」が空のままだとダメになったらしい。
というわけで、以下のようにTitleを適当に付加すると正常にPDFが作成された。

C++Builder10.2.3/Windows10(64bit/バージョン21H2)で確認。

[C++Builder]画像ファイルをテキストで保存、読込

C++Builderのフォーム情報(*.dfm)ではアイコン等の画像を16進文字列で保存しているようで、似たようなことをやってみる。(使いどころはパッとは思い浮かばないが)。

とりあえずVclの例。

サンプルプログラム

TFormにTButtonとTImageを2つずつと、TOpenPictureDialogを設置。TImageは2つともProportionalプロパティをtrueにしておく。

画像を16進文字列で保存

手順は以下の通り
①画像ファイルをTMemoryStreamに読み込む
②サイズの同じunsigned char配列を用意し、MemoryStreamからデータを書き込む
③1バイトずつ16進文字列に変換
④文字列をファイル保存

なお、C++Builderは、デフォルトではcharがsignedなので、明示的にunsigned charを指定している。

C:\Windows\Web\Wallpaper\Theme2\img7.jpgを読み込んだ結果:

保存されたテキストデータをエディタで見てみる。

テキストファイルから画像を読込、表示

復元手順は以下の通り。
①文字列のテキストファイルを読み込む
②2文字ずつ数値変換しunsigned char配列に格納
③MemoryStreamにデータを書き込む
④TImageにMemoryStreamから読み込む

復元して右側のImageに表示した結果:

FireMonkeyの場合

FireMonkeyでも、ほぼ同様なコードで可能。
違いは
・OpenPictureDialogがないので代わりにOpenDialogを使う
・Image関連のコードの修正(Pictureの代わりにBitmapを使う)
・jpeg/png/gif用のインクルード不要
だけ。
Image関連の違いは以下の通り。

対象ファイル

TImageに読込・表示できる画像ならおおおむね可能な模様。JPEGのほかPNG/BMP/GIF/TIF、さらにアイコンファイル(ico)でOKだった。メタファイル(emf)はVclでないと読み込めない模様。

ただし、C:\Windows\Web\Wallpaper\Theme1配下にあるWindows10壁紙用のjpeg画像は、VCLの場合だけ失敗した。テキストとして保存はできても、復元でTImageに読み込む時点で「サポートされていないストリーム形式」のエラーが出て、復元不能だった。理由は謎。

C++Builder10.2.3/Windows10(64bit/バージョン20H2)で確認。

続・C++Builderで異体字セレクタを使う サロゲートペアの場合

サロゲートペア文字にも異体字セレクタ対象の文字があることに、「C++Builderで異体字セレクタを使う」の投稿後に気付いた。

1.表示は簡単

基本は変わらない。対象文字のコードポイントの上位・下位バイトが分かっていれば、以下の数行のコードで表示できる。
なおTEditのフォントは、Windows8.1以降に標準バンドルされている游明朝を使っている。(オープンソースの花園明朝Aを使えば、より多くの異体字が表示できる)

表示結果

標準字形と異体字を並べてみる。

表示結果

2.両対応の汎用メソッド

実際にはコードポイントをいちいち探したり求めるのは面倒くさいので、実際の文字と異体字番号(ゼロから指定)から異体字を求める汎用メソッドを作ってみる。サロゲートかどうかも気にしなくてよい。
(以下のサンプルは、Stringが複数の文字からなる場合は、先頭文字の異体字を返す)

テストコード。
LabeledEditからIVS番号を取得して、文字を変換する。
2つのTEditのフォントは游明朝に設定している。

実行結果。

3.TRichEditの挙動が非サロゲートと異なる(?)

サロゲートペアの異体字をTRichEditに表示した場合、非サロゲートペアの場合と異なり、余分な空白が表示されないようだ。
異体字と標準字形を並べて表示してみると…。(左がTMemo、右がTRichEdit)

この挙動は謎。
なお、異体字の後ろにカーソルを置いた状態で、BackSpaceキーを押下すると〝先祖返り〟する挙動は、非サロゲート文字と変わらない。

C++Builder10.2.3とWindows10で確認。

C++Builderで異体字セレクタを使う

0.異体字セレクタとは

非常に大雑把かつ乱暴に言うと、山ほどある異体字を表示させるための仕組みのひとつ。
文字にいちいち固有の番号を割り当てるのではなく、番号に枝番号(符号)を付ける仕掛けである。
対応するフォントが必要だが、Windowsに標準添付の一部フォントでもそれなりに対応している。
詳しくはWikipediaなど参照してもらった方がいい。

Windows7からメモ帳でも利用可能になったので試してみた人も多いかもしれない。

1.表示は簡単

とりあえずよくある「1点しんにょうの辻」をTEditに表示する。
フォームにTEditとTButtonを一つずつ貼り付け、TEditのフォントはMS 明朝、フォントサイズは判別しやすいように30にしておく。
以下の数行のコードで、1点しんにょうの辻が表示される。

このように表示される。

2.仕組みとか

異体字セレクタは、通常の文字番号に、0xE0100から始まる枝番号を付けることで作動する。(最大で240までの枝番号が付けられるが、実際にそんなにたくさんの枝番号が定義された文字は現在ない。)
0xE0100は2byteで収まらないので、当然、wchar_tに格納できない。(C++Builderはwchar_tが2byte)
が、上位2byteは0xDB40で固定。下位2byteに0xDD00~0xDDEFまでのいずれかを格納してやればよい。

なお存在しない枝番を指定した場合、単に標準の文字が表示される。

3.フォントによって異なる枝番号

どの枝番号にどの異体字形が割り振られているかは、実はフォントによって違う。(正確に言うとフォントによって採用している字形コレクションが違う)。
異なるフォントを設定した3つのMemoに、「辻」の異体字の枝番を順に表示してみる。

実行結果

1点しんにょうの辻は、IPAmj明朝の場合は3番目、游明朝およびMS明朝なら1番目に割り振られているのが分かる。

いったん表示した後、フォントを変えると、文字が変化してしまう危険がある。

4.1文字で3文字分

文字数を数えるプログラムなどでは注意が必要。
SystemのLength()でもstd::wstringのlength()でも、3文字になる。
サロゲートペアの判定はIsSurrogateで簡単だが、異体字セレクタの場合はもう一工夫必要になるはず。

5.TMemoとTRichEditで挙動が異なる

異体字セレクタ文字は、TMemoとTRichEditで挙動・表示が違う。
1点しんにょうの辻と通常の辻の2文字をそれぞれ表示してみる。

実行結果。左がTMemo, 右がTRichEdit。

このようにRichEditは枝番分の文字の空きが取られてしまう。

ちなみに、この空きをキーボードで削除すると

枝番情報が消えるので最初の文字が通常の「辻」に戻ってしまう。

また、Memoの方でも、1点しんにょうの辻の後ろにカーソルを置きBackSpaceキーを押すと、文字は消えないがやはり先祖返りする。



以上、基本的にVclとFireMonkey共通。(ただしFireMonkeyにはRichEditはない。)
C++Builder10.2.3 / Windows10 で確認。

OpenTypeフォントにも望み通りの扁平・長体をかける

1.CreateFontIndirectではうまくいかない?

Windowsで、フォントに任意の扁平(長体)をかけて描画するには、CreateFontIndirect(もしくはCreateFont)Apiを使えばいい、はずだった。90年代のプログラミング本にはそうした記述があった記憶がある。が、現在、実際にはOpenTypeフォントや一部のTrueTypeフォントでは、うまくいかない。

OpenTypeフォントなら今時はDirectWriteを使うべきなのではという声もあるだろうが、20世紀からある由緒正しい(?)Windows Apiを使って何とかしてみよう、というお題。
なお、以下の記述は、Windows Apiの関連仕様をすべて正しく把握できている保証はなく、試行錯誤した経験則と推測によるもの。(突っ込み歓迎)

とりあえず試してみる。
基本的にはCreateFontIndirectに渡す適切なLOGFONT構造体を作るだけ。以下、扁平に必要なLOGFONT構造体を作る最低限のコード。

実際にVclアプリケーションで「MS ゴシック」と「游ゴシック」で試してみる。

フォント名以外、全く同一の指定をしているはずなのに、結果はこうなってしまう。

右側の「游ゴシック」の方は、明らかに扁平率が違う。
しかも、天地の描画位置も微妙にずれている。等間隔に線を描画してみると分かる。

2.TEXTMETRIC構造体にない3メンバ

さて、”おかしな扁平” “ずれた位置”になってしまうことに関係しそうなのは、TEXTMETRIC構造体になく、NEWTEXTMETRICEX構造体には存在する以下の3メンバ。
Microsoftの解説は英語しかないが、google翻訳で和訳。

メンバ 解説(Google翻訳)
ntmSizeEM フォントの正方形のサイズを指定します。 この値は概念上の単位(つまり、フォントが設計された単位)になります。
ntmCellHeight フォントの概念的な単位での高さを指定します。 この値は、ntmSizeEMメンバーの値と比較する必要があります。
ntmAvgWidth フォントの平均文字幅を概念上の単位で指定します。 この値は、ntmSizeEMメンバーの値と比較する必要があります。

分かったような分からないような説明だが、ともかく、このNEWTEXTMETRIC構造体の値は、昔ながらのApi EnumFontFamiliesExのコールバック関数EnumFontFamProcで取得できる。

コードは後回しにして、先の2フォントの値はこうなっている。

MS ゴシック 游ゴシック
ntmSizeEM 256 2048
ntmCellHeight 256 2636
ntmAvgWidth 128 1972

MSゴシックの場合はシンプルだ。ntmAvgWidthはntmSizeEMのちょうど半分。ntmSizeEMとntmCellHeightの値が全く同じ。
一方、游ゴシックはntmAvgWidthがntmSizeEMの半分より大きい。ntmCellHeightもtmSizeEMの値を上回る。

先ほどの描画結果とにらみ合わせてみると…。

以下のように推察される。
ntmAvgWidth……ntmSizeEMと同じサイズにしたい場合、この値をlfWidthに指定して初めて正方になる
ntmCellHeight……ntmSizeEMと同じサイズを指定した場合、描画時に、この値分の領域を上下に取る(つまりずれる)

3.3メンバを考慮した指定と描画

つまり、フォントに、望み通りの扁平(長体)をかけ、望み通りの位置に描画するには
(1)LOGFONTの指定時に、lfWidthにntmSizeEMとntmAvgWidthを考慮した値を指定する
(2)描画時の天地位置に、ntmSizeEMとntmCellHeightを考慮した値を指定する
ことが必要になる。

以下、サンプルコード。フォントごとにこれらの値を取得しておき、LOGFONT構造体作成時および描画時に値を調整する。必要な値はVclFormのコンストラクタで取得している。(丸め処理等は省略)

LOGFONT作成関数は以下のように書き換える

実際に描画するコードでは位置調整をする。

実行してみると、望み通りの扁平描画になる。

天地位置も正常。

あらゆるOpenTypeフォントでうまくいくのかどうかは不明だが、手持ちのOpenTypeフォントでは上手くいっている。
C++Builder10.2.3、Windows10で確認。

TDateの++演算子、欠落した日付の列挙

TDateには++演算子が使えることを今さら知った。値を一日ずつ進めてくれる。
比較演算子(<)もあるので、std::setやmapにも格納できる。

Vclアプリケーションでのテストコード。

出力結果:
2018/10/31
2018/11/01

もちろん閏月判定もやってくれる。

出力結果:
2015/02/28
2015/03/01
—————-
2016/02/28
2016/02/29
2016/03/01

前置と後置の評価タイミングも当然異なる。

出力結果:
postfix : equal
prefix : differ

もともと与えられた日付の一覧から、欠落した日付を列挙する方法を調べていて気付いたので、以下、そのサンプル。

出力結果:
2016/02/26
2016/02/28
2016/02/29
2016/03/04
2016/03/05
2016/03/08

なおTDateはTDateTimeのtypedefなので、TDateTimeでも同様に++演算子で1日ずつ値が進む。

出力結果:
2018/11/30 14:35:21
2018/12/01 14:35:21
2018/12/02 14:35:21

Vcl/FireMonkey共通。
C++Bulider10.2.3 / Windows10で確認。

ディレクトリ・ファイル操作の新旧方式

C++Bulider(RadStudio)のディレクトリやファイル操作に関しては、昔ながらのSystem.SysUtils配下の方法と、新しいSystem.IOUtils配下の方法がある。
まったく同一ではないが、基本的な操作は、どちらも似たようなことが出来る。

どちらでもいいといえば良いのだが、クロスプラットフォームを前提に開発するなら、新しいIOUtils配下にしかない関数群を使った方が便利なことが多いと思う。

どうせなら、Windows単体の開発でも、ふだんから新方式を使って慣れておいた方がいいというのが個人的な考え。

主な操作のそれぞれのやり方をまとめてみた。
(引数のpathはフルパスのディレクトリ名またはファイル名)

以下、VclとFiremonkey共通。

使い方
Includeファイル名
SysUtils System.SysUtils.hpp
IOUtils System.IOUtils.hpp
ファイル名からドライブ・ディレクトリ部分を得る
SysUtils ExtractFileDir( path )
IOUtils TPath::GetDirectoryName( path )
ディレクトリ名とファイル名からフルパスのファイル名を作る
SysUtils IncludeTrailingPathDelimiter( path )+filename
IOUtils TPath::Combine(path,filename)
ディレクトリの存在確認
SysUtils DirectoryExists(path)
IOUtils TDirectory::Exists(path)
ファイルの存在有無
SysUtils FileExists(path)
IOUtils TFile::Exists(path)
ディレクトリの作成
SysUtils CreateDir(path)
IOUtils TDirectory::CreateDirectory(path)
空のディレクトリを削除
SysUtils RemoveDir(path)
IOUtils TDirectory::Delete(path)
ディレクトリを中身ごと削除
SysUtils (自前で実装の必要あり)
IOUtils TDirectory::Delete(path,true)
ファイルを削除
SysUtils DeleteFile(path)
IOUtils TFile::Delete(path)

C++Builder Berlin Update2 / Windows7で確認。

TRectの便利関数

どのバージョンからか把握していないが、TRectにはいろいろな関数(やコンストラクタ)が実装されている。
Vclの一昔前のサンプルだと、こうした関数を使わない(ちょっと面倒くさい)処理が紹介されていたりする。
というか自分自身も昔の名残で、ついだらだらとコードを書きがちだったり。

Helpの一覧だと、実装が書いてあるだけで(RadStudio10.2 Tokypリリース直後時点)、まあ読めば分かるといえばそうなのだが、忘れがちなので一覧にしてみた。

System配下なので、Vcl・FireMonkey共通。(Delphiでも同じように使えるはず。)
以下、RはTRect、PtはTPointの変数。

動作 実例
PointからRectを作成 R = TRect(Pt1,Pt2)
Rectの値を正しくする R.Normalize()
位置をずらす R.Offset(X,Y)
R.Offset(Pt)
Rectの始点を指定する R.SetLocation(X,Y)
R.SetLocation(Pt)
Rectの始点を得る Pt = R.GetLocation()
Rectに点が含まれるか bool b = R.Intersects(Pt)
他のRectと重なっているか bool b = R1.Overlaps(R2)
他のRectを完全に含むか bool b = R1.Contains(R2)
全方向に大きく(小さく)する R.Inflate(X,Y)
R.Inflate(l,t,r,b)
中心点を得る Pt = R.CenterPoint()

FireMonkeyのイベント等で使われるTRectF/TPointFでもおおむね同様の関数が使える。(一部名前が違ったりするようだが、精査していない)