• SAStrutsのAction

    2012年03月29日 21時52分
    前回の続きでもう1パターン考えてみました。

    やっぱり一々getter作るのは、書くのも読むのも面倒だよね。
    ということで、publicフィールドで行く方向で考えます。

    要するにリクエストパラメータのセットから特定のフィールドを除外してやれば良いわけです。
    「特定のフィールド」を指定するにはやはりアノテーションが良いでしょう。
    以下のアノテーションを作成します。
    1@Target(ElementType.FIELD)
    2@Retention(RetentionPolicy.RUNTIME)
    3public @interface Readonly
    4{
    5}



    リクエストパラメータのセットはS2RequestProcessorで行っているので、これを改造してしまいます。
    1public class RequestProcessor extends S2RequestProcessor
    2{
    3    @SuppressWarnings({"unchecked""rawtypes"})
    4    @Override
    5    protected void setSimpleProperty(Object bean, String name, Object value)
    6    {
    7        if (bean instanceof Map)
    8        {
    9            setMapProperty((Map)bean, name, value);
    10            return;
    11        }
    12
    13        BeanDesc beanDesc = BeanDescFactory.getBeanDesc(bean.getClass());
    14        if (!beanDesc.hasPropertyDesc(name))
    15            return;
    16
    17        PropertyDesc pd = beanDesc.getPropertyDesc(name);
    18        if (!pd.isWritable())
    19            return;
    20
    21        // @Readonlyアノテーションが有ったらセットしない
    22        Field f = pd.getField();
    23        if (f != null && f.getAnnotation(Readonly.class) != null)
    24            return;
    25        
    26        if (pd.getPropertyType().isArray())
    27            pd.setValue(bean, value);
    28        else if (List.class.isAssignableFrom(pd.getPropertyType()))
    29        {
    30            List<String> list = ModifierUtil.isAbstract(pd.getPropertyType()) ? new ArrayList<String>() : (List<String>)ClassUtil.newInstance(pd.getPropertyType());
    31            list.addAll(Arrays.asList((String[]) value));
    32            pd.setValue(bean, list);
    33        }
    34        else if (value == null)
    35            pd.setValue(bean, null);
    36        else if (value instanceof String[])
    37        {
    38            String[] values = (String[]) value;
    39            pd.setValue(bean, values.length > 0 ? values[0] : null);
    40        }
    41        else
    42        pd.setValue(bean, value);
    43    }
    44}


    struts-config.xmlを書き換えてセットします。
    <controller
            maxFileSize="1024K"
            bufferSize="1024"
            processorClass="jp.ku6.seasar.struts.action.RequestProcessor"
            multipartClass="org.seasar.struts.upload.S2MultipartRequestHandler"/>



    使い方は、publicなフィールドに@Readonlyを付けるだけです。
    1@Readonly
    2public List<Homu> homus;



    あと、publicにするとAutoBindingの対象になるので、一応ActionのAutoBindingDefを変更してみました。
    <component name="actionCreator" class="org.seasar.framework.container.creator.ActionCreator">
    		<initMethod name="setAutoBindingDef">
    			<arg>@org.seasar.framework.container.assembler.AutoBindingDefFactory@SEMIAUTO</arg>
    		</initMethod>
    	</component>


    別に対象になっても実際に設定されることはまず無いとは思うのですが、
    どうせ設定したい物には明示的に@Resourceを付けるので一応やっておくかなと。


    何かだめなところが見つかるまではこれで行ってみます。
  • SAStrutsのActionのトランザクション

    2012年03月28日 22時31分
    SAStruts + Mayaaで開発しています。
    まだ実装方法に迷いながらやっているので、考えたことなどをメモっておこうと思います。


    表示用データを作ってHTMLに出力する流れを次のようにしていました。


    まず、Actionに表示用データのフィールドを持ちます。
    リクエストパラメータの設定対象とならないようprivateにしています。
    1private List<Homu> homus;


    privateだとMayaaから参照出来ないため、getterを作成します。
    1public List<Homu> getHomus()
    2{
    3    return homus;
    4}


    そしてMayaaから上記getter経由でデータを読み出します。
    <m:forEach id="homus" items="${homus}" var="homu"/>


    この時、SAStrutsのActionがデフォルトでTxAttributeCustomizerを設定しているため、
    getHomus()メソッドにトランザクションがかかってしまいます。

    このトランザクションは特に必要な物ではないため、消してみようかと思いました。



    解決策1

    動くんだから気にしない。


    解決策2

    publicにしてgetterを使わない。

    チュートリアルなんか見ていると、SAStruts的な正解はpublicにするような気もします。
    1public List<Homu> homus;


    ただ、リクエストパラメータで同じ名前を指定されるとデータが入るのが気になります。
    パラメータ名は外からは推測しか出来ませんし、Actionできちんと上書きすれば問題ないか。


    解決策3

    TxAttributeCustomizerを拡張して、トランザクションが不要なメソッドを対象から外す。

    具体的にはTxAttributeCustomizerを継承し、
    @Executeアノテーションが付いたメソッド以外は除外するクラスを作る。
    1public class ActionTxAttributeCustomizer extends TxAttributeCustomizer
    2{
    3    @Override
    4    protected void doCustomize(final ComponentDef componentDef)
    5    {
    6        final Class<?> componentClass = componentDef.getComponentClass();
    7        final TransactionAttribute classAttribute = componentClass.getAnnotation(TransactionAttribute.class);
    8        final TransactionAttributeType classAttributeType = classAttribute != null ? classAttribute.value() : defaultAttributeType;
    9
    10        for (final Method method : componentClass.getMethods())
    11        {
    12            if (method.isSynthetic() || method.isBridge())
    13                continue;
    14
    15            if (method.getDeclaringClass() == Object.class)
    16                continue;
    17
    18            // @Executeアノテーションが付いていない物は除外する
    19            if (method.getAnnotation(Execute.class) == null)
    20                continue;
    21
    22            final TransactionAttribute methodAttribute = method.getAnnotation(TransactionAttribute.class);
    23            final TransactionAttributeType methodAttributeType = methodAttribute != null ? methodAttribute.value() : classAttributeType;
    24            final String interceptorName = txInterceptors.get(methodAttributeType);
    25            if (!StringUtil.isEmpty(interceptorName))
    26                componentDef.addAspectDef(AspectDefFactory.createAspectDef(interceptorName, method));
    27        }
    28    }
    29}

    「get」から始まるメソッドは除外。とかでもいいかもしれません。



    そこはかとなくこれでいい気がしないことも無くも無いのでこれで行ってみようと思います。
  • D言語用 SDLモジュール version 1.2.15.0

    2012年03月16日 20時33分
    SDLが1.2.15にバージョンアップしていたので合わせて更新しました。

    sdl_1_2_15_0.zip


    SDL本体以外のモジュールも更新しています。
  • DirectX モジュール 1.0.3

    2012年02月20日 20時16分
    D言語(D2)用 DirectX モジュール

    コンパイルエラーが残っていたので修正しました
    libファイルも追加しました
  • Firefoxボタンの謎

    2012年02月18日 17時52分
    FirefoxをWindows7で実行すると、左上に「Firefox」ボタンが付いています。

    Firefoxボタン



    今まで何も考えずに使っていましたが、
    ふと、「これどうやって付けてるんだ?」と思ったので調べてみました。

    詳しく調べるならFirefoxのソースを読むのが良いと思うのですが、
    ソースをダウンロードしたものの、あまりの量に負けてしまいました。
    外からの見た目が大体同じ物を作るということとご理解ください。


    注意して見てみると、似たような物が結構見つかります。(特にMicrosoft製品)
    IEもOfficeもタイトルバーが伸びていて、その中にタブやボタンが配置されています。

    これだけ使われているということは、
    Hack的な実装方法ではなく、きちんとした実装方法がありそうです。


    調べてみるとやはり公式の資料が見つかりました。
    Custom Window Frame Using DWM(Microsoft公式)


    基本的には上記のページを見れば実装できます。
    内容が被るところが多いですが、私が試したことを以下に記載してみようと思います。


    フレームを広げる

    ボタンを配置する前に、フレームを広げる方法です。

    ボタンやタブを配置するためにはある程度広い面積が必要になるため、
    ほとんどのアプリはフレームを広げています。(主にタイトルバー)

    フレームを広げるには、DwmExtendFrameIntoClientArea関数を使用します。

    Windows Vistaから使用されているDWMに対する操作系の関数です。
    クライアント領域までフレームを拡張することが出来ます。

    第1引数に対象ウィンドウのハンドルを、
    第2引数のMARGINSに拡張する量を指定します。
    (拡張していくつになるかではなく、いくつ拡張するかを指定します。)

    例えば、ウィンドウ上部のフレームを64ピクセル拡張するには次のように書きます。
    1MARGINS margins;
    2margins.cyTopHeight = 64;
    3margins.cyBottomHeight = 0;
    4margins.cxLeftWidth = 0;
    5margins.cxRightWidth = 0;
    6DwmExtendFrameIntoClientArea(windowHandle, &margins);


    DwmExtendFrameIntoClientAreaを呼び出した後には、SetWindowPosをSWP_FRAMECHANGED指定で呼び出して更新します。
    1RECT rect;
    2GetWindowRect(windowHandle, &rect);
    3SetWindowPos(windowHandle, null, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_FRAMECHANGED);


    これらの処理をWM_CREATEに記述すれば次のようにフレームが拡張されます。

    フレームを拡張したウィンドウ


    フレームをクライアント領域にする

    実際にどうやってフレームに書き込むかというと、フレームをクライアント領域として扱います。
    クライアント領域ならば画像でもなんでも書くのは簡単です。

    そうなるとフレームのガラスっぽい部分も自分で書く必要がありそうですが、
    DWMの仕様として「フレームがクライアント領域でも、黒(ARGB(0,0,0,0)の部分はフレームを描画する」という動作になっています。
    (より厳密には、「フレームとクライアント領域のデータがアルファブレンドされる」という動作です)

    例として先ほどフレームを拡張したウィンドウのクライアント領域を白で塗りつぶすと次のようになります。
    白背景

    さらに半透明の赤で塗りつぶすとこうなります
    半透明赤背景

    つまり、ボタンやタブを自分で描画し、それ以外の部分を黒にしておけば、
    DWMが勝手に合成して、フレーム上にボタンやタブが置いてあるように見えます。


    フレームをクライアント領域にするには、WM_NCCALCSIZEを処理します。
    lParamがNCCALCSIZE_PARAMSのポインタになっているので、
    その内部の矩形範囲を指定することでクライアント領域を調整できます。

    左右と下のフレームを通常通り非クライアント領域に、
    上のフレームをクライアント領域に設定するには次のように書きます。
    1int x = GetSystemMetrics(SM_CXSIZEFRAME);
    2int y = GetSystemMetrics(SM_CYSIZEFRAME);
    3
    4NCCALCSIZE_PARAMS* pncsp = cast(NCCALCSIZE_PARAMS*)lParam;
    5pncsp.rgrc[0].top    = pncsp.rgrc[0].top    + 0;
    6pncsp.rgrc[0].bottom = pncsp.rgrc[0].bottom - y;
    7pncsp.rgrc[0].left   = pncsp.rgrc[0].left   + x;
    8pncsp.rgrc[0].right  = pncsp.rgrc[0].right  - x;


    先ほどの半透明赤背景だとこうなります。
    半透明赤背景2


    フレームサイズを色々変えてみたのですが、
    左右と下のフレーム幅は自由に決められるものの、
    上のフレームを本来のタイトルバーの高さより小さくすると表示がずれるようです。(0だけは例外的に大丈夫)

    この辺りは資料が少ないので使い方を間違っているのかもしれません。


    拡張したフレーム部分と非クライアントの衝突判定

    ここまで実装すれば、あとはフレームと被ったクライアント領域に描画すればボタン等の表示は可能です。
    ボタン表示


    ただ、このままだとクライアント領域にしたフレーム部分を非クライアント領域として扱えません。
    (フレームをドラッグしてウィンドウを移動したり、ダブルクリックで最大化したりが動かない)

    そのため、WM_NCHITTESTに接触判定処理を追加します。
    WM_NCHITTESTの戻り値として、HTTOPやHTCAPTIONを返せば、
    マウスカーソルが上の枠やキャプションと接触しているとして扱われます。

    具体的な例は次のとおりです。
    1// マウスカーソル位置を取得
    2int x = LOWORD(lParam);
    3int y = HIWORD(lParam);
    4
    5// 枠のサイズを取得
    6int borderYWidth = GetSystemMetrics(SM_CYSIZEFRAME); // 水平枠の高さ
    7int borderXWidth = GetSystemMetrics(SM_CXSIZEFRAME); // 垂直枠の幅
    8
    9// 現在のウィンドウサイズを取得
    10RECT windowRect;
    11GetWindowRect(windowHandle, &windowRect);
    12
    13// 左右の枠を避ける
    14if (windowRect.left + borderXWidth <= x && x < windowRect.right - borderXWidth)
    15{
    16    // 上の水平枠と接触しているか返す
    17    if (windowRect.top <= y && y < windowRect.top + borderYWidth)
    18        return HTTOP;
    19    // CAPTIONと接触しているか返す
    20    else if (windowRect.top + borderYWidth <= y && y < windowRect.top + 32/*フレームサイズ*/)
    21        return HTCAPTION;
    22}
    23
    24// 上記以外の領域との接触判定はデフォルトに投げる


    あと、これをやっても何故か右クリックでシステムメニューを出すのが動かないので
    WM_NCRBUTTONUPを捕まえて自分で処理します。
    1if (wParam == HTCAPTION)
    2    TrackPopupMenu(GetSystemMenu(windowHandle, FALSE), 0, LOWORD(lParam), HIWORD(lParam), 0, windowHandle, null);



    サンプル

    実際に動作を試したソースを置いておきます。
    ソース


    一応動いていますが、Windows的に正しい自信がないので、より正しい方法があれば教えてください。
    というか、Microsoftがもっと丁寧に書くべき。