キャラクター置時計(1)(2003/03/16)
新着情報 トップメニュー ボードメニュー 掲示板 お手紙はここ!

1って。……2があるとでも?

プログラミング自体は継続してやっていますが、ことさら書くほどのことも無く……まぁ、ドキュメントとか見ればそれなりに苦労は伺えるかと思いますが。



さて正弓さんとこが 30000 アクセス達成とゆうことで描いた途中絵(既に消滅)を拝借してキャラクター置時計を作ってみました。

©國杜正弓 2003
こーゆーのはリージョンという仕組みを使ってすぐできるんですよ。ペル●ナとか ActWin とかに影響を受けていたときにも、やはり正弓さん絵をパクってきて時報ソフトを作ったりしましたし、SSTP サーバーを作るときにもあれこれと実験したモンです。いやあ、懐かしい思い出だなぁ(゚∀゚ )ノ

と、言うわけで基本的に当初の予定で言えば完成レベルなんですが、β1をば配布します。



今回は作業的には今までのコードをコピーペーストして完成させています。ざっと思い出すだけでも
・画像データからリージョン抽出
・ウィンド全体に対するクリック→ドラッグによるウィンドウ移動
・タスクトレイの格納とタスクパーからの撤去(この二つの処理は別々にやるんですよ)
が挙げられます。

新規の作業としては
・リージョンデータのキャッシュファイルへの保存
・時計表示機能の分離とデジタル・アナログ時計実装
があります。順を追って説明いたしましょうかね。



このソフトでのリージョンの抽出には以下の関数・手続きを使用しています。実際には ExtractRegion() はむ関数内手続きの形で書きましたが見難いので分離しておきました。


function ExtractRegion( Image:TBitmap ): HRGN;
var
    W: Integer;
    H: Integer;
    X: Integer;
    Y: Integer;
    P: Integer;
    C: TColor;
    T: TColor;
    F: TForm;
    L: TLabel;
    A: Integer;
    Z: Integer;
    R: HRGN;
begin
    Result := CreateRectRgn(0,0,0,0);
    P := 0;

    W := Image.Width;
    H := Image.Height;
    T := Image.TransparentColor and $00FFFFFF;
    F := TForm.Create( Self );
    F.BorderStyle  := bsToolWindow;
    F.BorderIcons  := [];
    F.Caption      := 'DMS 初期化中...';
    F.ClientWidth  := 200; 
    F.ClientHeight := 40; 
    F.Position     := poDesktopCenter;
    L := TLabel.Create( F );
    L.Parent := F;
    L.Left   := (F.ClientWidth  - F.Canvas.TextWidth('00% 完了')) div 2;
    L.Top    := (F.ClientHeight - L.Height                      ) div 2;
    Z := 0;
    F.Show;

    while (P div W < H) and (not Application.Terminated) do
    begin
        X := P mod W;
        Y := P div W;
        C := Image.Canvas.Pixels[X,Y] and $00FFFFFF;
                
        if (C <> T) then
        begin
            R := CreateRectRgn( X, Y, X+1, Y+1 );
            CombineRgn( Result, Result, R, RGN_OR );
            DeleteObject( R );
        end;

        Inc( P );

        A := (P*100) div (W*H);
        if (A <> Z) then
        begin
            L.Caption := IntToStr(A) +'% 完了';
            Application.ProcessMessages;
            Z := A;
        end;
        
    end;

    F.Release;
end;

function TForm1.GetRegion( ForceInitial:Boolean=False ): HRGN;
var
    FN : TFileName;
    AEN: TFileName;
    BMP: TBitmap;
    Rgn: HRGN;
    Sz : DWORD;
    pRD: PRgnData;
begin
    FN := ChangeFileExt( Application.ExeName, '.rgn' );
    AEN:= Application.ExeName;
    if ForceInitial or not FileExists( FN ) then//or (FileAge(AEN) > FileAge(FN) ) then
    begin
        (* imgChar は実は TGLDPNG だったりします。Bitmap に変換してるわけ。 *)
        BMP := TBitmap.Create;
        BMP.Assign( imgChar );
        Result := ExtractRegion( BMP );
        BMP.Free;
        
        (* そのリージョンに、時計を描画する領域(擬似ウィンドウ)を追加 *)
        Rgn := ClockWork.GetWindowRectRgn;
        CombineRgn( Result, Result, Rgn, RGN_OR );
        DeleteObject( Rgn );
        
        (* 出来上がったリージョンは、バイナリ化して保存しておく *)
        Rgn := CreateRectRgn( 0,0,0,0 );
        CombineRgn( Rgn, Rgn, Result, RGN_OR );
        Sz  := GetRegionData( Result, 0, NIL );
        pRD := AllocMem( Sz );
        GetRegionData( Result, Sz, pRD );
        DeleteObject( Rgn );

        with TMemoryStream.Create do
        begin
            Write( pRD^, Sz );
            SaveToFile( FN );
            Free;
        end;
        
        FreeMem( pRD );
        
    end else begin
        (* キャッシュファイル発見 読み込んでリージョンへと変換 *)
        (* ExtCreateRegion() が必要としているのは実体であることに留意 *)
        with TMemoryStream.Create do
        begin
            LoadFromFile( FN );
            Sz := Size;
            pRD:= AllocMem( Sz );
            Read( pRD^, Sz );
            Free;
        end;
        
        Result := ExtCreateRegion( NIL, Sz, pRD^ );
        
        FreeMem( pRD );
    end;
end;

解かりますかね? そのまんま、ですよね?
一般に、TBitmap.Pixels[] での読み込みはスピードが遅いと不評でして、クラスが提供しているポインタから読み出すのが高速化のコツです。しかも一々 CombineRegion() API しないで矩形バッファを自前で作って ExtCreateRegion() API するともっと早くなります。……が、手元の環境ではデバッグするのに十分な速度が出ていますし、一度作ったリージョンはディスクに保存するのでそう度々作り直す必要はありません。
だもんで今はこれに甘んじておこうかと思います( ̄▽ ̄)

この関数で作ったリージョンハンドルは、SetWindowRegion() API に渡してメインフォームに適用しています。
しかし、ただ適用しただけではキャラクターは出ません……それらしき輪郭が出てくるだけです(笑) なので、フォームの OnPaint イベントで上に渡したのと同じ TBitmap によって自分自身を描画(TBitmap.Canvas.Draw)させればキャラクターフィギュア≠フできあがり。まー、なんてお手軽なんざましょ!

ちなみにリージョンのハンドルは、SetWindowRegion() API に渡すと破棄されます。もしこのリージョンを流用する必要があるなら、この API に渡す前に複製しておきましょう。

Rgn1 := CreateRectRgn( 0,0,0,0 );
CombineRgn( Rgn0, Rgn1, 0, RGN_COPY );

こうすると Rgn1 を複製して Rgn0 へ格納します。Rgn0 := Rgn1 なんてオオボケはやらないように!!

逆にいうと、ただちにウィンドウへ適用するなら変数無しで API の戻り値を直に渡しても構わんみたいです。



次に Tips なんぞ 2件ほど。まずはウィンドウをドラッグして移動する方法。この方法は Windows3.x のころからある古典的なやり方です。WM_NCHITTEST をフックするやり方もありますが、後々マウスへのリアクションを色々実装するとなると厄介なのでパスしました。

まずフォームの private フィールドに


procedure WMLButtonDown( var MSG:TWMLButtonDown ); message WM_LBUTTONDOWN;

と書き、次のように実装します。


procedure TForm1.WMLButtonDown(var MSG:TWMLButtonDown );
begin
	SendMessage( Handle, WM_SYSCOMMAND, SC_MOVE or 2, 0 );
end;

これでキャプション以外の部分でもドラッグすると移動します。
こんないい加減なコードでもダブルクリックとかはちゃんと今までどおりに認識するのでたいしたものだと思います。クリックした位置によって別の処理をしたいなら、引数の構造体からマウスの位置を取り出してください。



次にタスクトレイへの常駐。注意すべき点として、エクスプローラがクラッシュした際にトレイへ復活させられるテクがある、ということ。以下のメソッドを実装して適当なイベントから呼び出してください。


procedure TForm1.TaskTrayProcs( var Msg:TMessage );
var
    P  : TPoint;
    pum: TPopupMenu;
begin
    pum := NIL;

    case Msg.LParam of
        WM_LBUTTONDOWN  : SetForegroundWindow( Handle );
        WM_LBUTTONDBLCLK: OnDblClick( NIL ); // ダブルクリックイベントへ丸投げ( ̄▽ ̄;)
        WM_RBUTTONDOWN  : pum := PopupMenu;
    else
        if (Msg.LParam = LongInt(UM_TrayIconRecreate)) then
            AddToTaskTray
        else
            Msg.Result := DefWindowProc( NID.Wnd, Msg.Msg, Msg.wParam, Msg.lParam );
    end;
    
    if Assigned(pum) then
    begin
        GetCursorPos( P );
        SetForegroundWindow( Handle );
        pum.Popup( P.X, P.Y );
    end;
end;

procedure TForm1.AddToTaskTray;
begin
    if TaskTray and not FTrayUsed then
    begin
        NID.cbSize := SizeOf( TNotifyIconData );
        NID.hIcon  := LoadIcon( HInstance, 'TRAYICON' );
        NID.szTip  := sMutexText;
        NID.uFlags := NIF_ICON or NIF_MESSAGE or NIF_TIP;
        NID.uID    := 0;
        NID.Wnd    := Classes.AllocateHWnd( TaskTrayProcs );
        NID.uCallbackMessage := UM_TASKTRAY;

        FTrayUsed := Shell_NotifyIcon( NIM_ADD, @NID );
    end;
end;

procedure TForm1.RemoveFromTaskTray;
begin
    if FTrayUsed then
    begin
        Shell_NotifyIcon( NIM_DELETE, @NID );
        Classes.DeallocateHWnd( NID.Wnd );
        FTrayUsed := False;
    end;
end;

次の変数・定数をどこかで宣言しなくてはなりません。


var
    UM_TrayIconRecreate: UINT;
    NID : TNotifyIconData; {in ShellAPI}
const
    UM_TASKTRAY = WM_USER +$1234;

更に、OnCreate などでメッセージとして登録する必要があります。


UM_TrayIconRecreate := RegisterWindowMessage( 'TaskbarCreated' );



タスクバーに表示させない方法は少々トリッキーです。Delphi ならではの仕様を利用していますが将来的には大丈夫なんでしょうか?(^-^;)

適当なイベントなどから

ShowWindow( Application.Handle, SW_HIDE )

を実行してください。出し方は……判りますよね?



とまぁ、こんなところでしょうかね。ソースコードは今の時点のものだけアップロードします。

この後、メッセージ表示機能とか搭載して、指定URLからメッセージをダウンロードするような方向に進むと思います……。ふふふのふ。


新着情報 トップメニュー ボードメニュー 掲示板 お手紙はここ!