うならぼ

申し訳程度のアフィリエイトとか広告とか解析とかは/aboutを参照

HTML+CSSで宛名印刷

あけましておめでとうございます。

近頃のCSSwriting-mode: vertical-rl で縦書きができます。皆さんご存知の通り mm での位置指定は余裕ですし、 page-break-after で改ページもできます。宛名印刷できますね。

というわけで今年の年賀状の宛名面のメイキングです。

なお下のQiita記事に触発され、土台部分を参考にしています。

qiita.com

レイアウト

基本となるCSSはこれだけです。ほとんど前掲のQiita記事の通りですが、調整した点はコメントに記載しました。

/* UAデフォルトのマージンを除去 */
html, body { margin: 0; padding: 0; }

@page {
        /* はがきサイズ。手元の環境では用紙サイズの自動選択は働かなかった。 */
    size: 100mm 148mm;
    margin: 0;
}

@media screen {
    body {
        background: #eee;
    }
    .page {
        background: white;
        box-shadow: 0 .5mm 2mm rgba(0,0,0,.3);
        margin: 5mm;
                /* 一覧しやすいようにfloatさせる */
        float: left;
    }
}

.page {
    width: 100mm;
    height: 147mm;
    page-break-after: always;
        /* ページ上の配置を position: absolute で行うため */
    position: relative;
}

あとは好きなものを、好きな位置に、好きなフォントで配置するだけ。

といっても何の参考もなしにやるのはつらいので、はがきの画像をスキャンなりしてページの背景にしておくと位置調整の参考になります。また、他のソフトでだいたいのレイアウトを決めてから、その画像や数値を使うのも手です。

その他メモ。

  • bottomで位置指定 + min-height + text-align: left/center で「普段は下に伸びるけど足りなければ上の余白も使う」的なレイアウト。
  • 縦中横は text-combine というCSSプロパティもあるが、愚直に一行分の幅に詰め込んでしまうのでつらい。実際にはちょっとはみ出すぐらいにしたかったので、結局 display: inline-block を使った。
  • line-height 、複数行のとこはどうせいじるので、一行の要素向けにデフォルト 1 でよさそう。

JavaScriptで流し込む

CSSでのレイアウトが終わったものの、住所録をCSVから取り込んだりすることを考えると、

  • 郵便番号の前半と後半を分けたい、とか
  • 住所の数値は別のスタイルを適用したいからタグで囲みたい、とか
  • 宛名を均等割り付けしたい、とか
  • そもそもテンプレートに流し込みたい、とか

色々と課題が残っています。せっかくHTMLなのですからJavaScriptで解決しましょう。

今回はあらかじめどうにかして*1 <page name="山田太郎" postal="1030027" addr1="東京都中央区..."></page> みたいな形に変形しておいて、そこから先をRiotでやることにしました。

タグ定義はJadeで書きました。

format-addr
    script.
        let text = opts.text;
        text = text.replace(/-/g, 'の');
        let wrap_numbers = content => {
            let elem = document.createElement("span");
            elem.className = "numbers";
            elem.textContent = content;
            return elem;
        };
        let re = /(\w+)|([^\w]+)/g;
        for (let m; m = re.exec(text);){
            if(m[1])
                this.root.appendChild(wrap_numbers(m[1]));
            else
                this.root.appendChild(document.createTextNode(m[2]));}
page
    section.page
        div.to-postal
            | {opts.postal.substr(0,3)}
            span.right: {opts.postal.substr(3,4)}
        div.to-addr
            format-addr(text='{opts.addr1}')
            div.rest: format-addr(text='{opts.addr2 || ""}')
        div.to-name
            | {opts.name} 様
    script.
        // justify
        this.on('updated', function() {
            var range = document.createRange();
            for (var elem of document.querySelectorAll('.to-name')) {
                range.selectNodeContents(elem);
                elem.style.letterSpacing = 0;
                elem.style.letterSpacing = (elem.clientHeight - range.getBoundingClientRect().height) / elem.innerText.length;
            };
        });

先ほどの課題については次のように解決しています。

  • 郵便番号の前半と後半を分ける→ String.prototype.substr()
  • 住所の数値をタグで囲む→ this.root 上に構築する別のカスタムタグを作成
  • 宛名を均等割り付け→ Rangeオブジェクトで取得した文字列の描画領域とボックスの高さを元に letter-spacing を調整

あとは body 内に上記で定義したpageタグを並べるだけ。

*1:Dapper+OLEDBでExcelから吸い出してタグ形式にするスクリプトをLINQPadで書いた

F#のIEventに求められる「標準のデリゲート型」の定義、もしくはdelegate制約の話

C#などで書かれたクラスにF#からアクセスする際、EventHandler デリゲートと互換性のあるイベントは IEvent<'Delegate,'Args> としてアクセスすることができます。

互換性のないデリゲート型を IEvent<'Delegate,'Args'>'Delegate 型引数に指定するとコンパイルエラーになります。互換性といっても、AssemblyLoadEventHandler のように EventHandler<'T> にキャストできないような型でもいいのですから、継承関係では判断することができません。どうやって検証しているのでしょうか。

ソースコードを探検する

リファレンス には次のような定義が書かれています。

type IEvent<'Delegate,'Args when 'Delegate : delegate<'Args,unit> and 'Delegate :> System.Delegate> =
interface
inherit IObservable<'Args>
inherit IDelegateEvent<'Delegate>
end

when 'Delegate : delegate<'Args,unit> という型制約が気になりますね。言語リファレンスを読んでみましょう。

Delegate Constraint : delegate<tuple-parameter-type, return-type>

The provided type must be a delegate type that has the specified arguments and return value; not intended for common use.

https://docs.microsoft.com/en-us/dotnet/articles/fsharp/language-reference/generics/constraints

デリゲートを構成する型で制約をかけられるように見えますが、件のデリゲートは 'Args -> unit ではなく obj * 'Args -> unit のはずです。どういうことでしょうか。

次のコードもコンパイルエラーになってしまいます。

let test (value:'T when 'T : delegate<int,unit> and 'T :> Delegate) = ()
type Foo = delegate of int -> unit

test (new Foo(fun x -> () ))
// error FS0001: 型 'Foo' には標準ではないデリゲート型があります

delegate制約の挙動を掘り下げる必要がありそうです。いざソースコードへ。

fsharp/ConstraintSolver.fs at 8dcf06f949dc9d05d35aa6bab0fbbd4911d480f3 · fsharp/fsharp · GitHub

and SolveTypIsDelegate (csenv:ConstraintSolverEnv) ndeep m2 trace ty aty bty =
    trackErrors {
        let g = csenv.g
        let m = csenv.m
        let denv = csenv.DisplayEnv
        if isTyparTy g ty then 
            return! AddConstraint csenv ndeep m2 trace (destTyparTy g ty) (TyparConstraint.IsDelegate(aty,bty,m))
        elif isDelegateTy g ty then 
            match TryDestStandardDelegateTyp csenv.InfoReader m AccessibleFromSomewhere ty with 
            | Some (tupledArgTy,rty) ->
                do! SolveTypEqualsTypKeepAbbrevs csenv ndeep m2 trace aty tupledArgTy 
                do! SolveTypEqualsTypKeepAbbrevs csenv ndeep m2 trace bty rty 
            | None ->
                return! ErrorD (ConstraintSolverError(FSComp.SR.csTypeHasNonStandardDelegateType(NicePrint.minimalStringOfType denv ty),m,m2))
        else 
            return! ErrorD (ConstraintSolverError(FSComp.SR.csTypeIsNotDelegateType(NicePrint.minimalStringOfType denv ty),m,m2))
    }

delegate制約の tuple-parameter-typeatyreturn-typebty に対応します。そしてこれらが対象の型である ty と比較されているのか追っかけてみると、TryDestStandardDelegateTyp という関数に行きつきます。もう答えがほとんど見えていますが、この実装も確認してみます。

fsharp/InfoReader.fs at 35d22f8d2b4b39744060709d20528afa31737ae7 · fsharp/fsharp · GitHub

/// Try and interpret a delegate type as a "standard" .NET delegate type associated with an event, with a "sender" parameter.
let TryDestStandardDelegateTyp (infoReader:InfoReader) m ad delTy =
    let g = infoReader.g
    let (SigOfFunctionForDelegate(_,compiledViewOfDelArgTys,delRetTy,_)) = GetSigOfFunctionForDelegate infoReader delTy m ad
    match compiledViewOfDelArgTys with 
    | senderTy :: argTys when (isObjTy g senderTy) && not (List.exists (isByrefTy g) argTys)  -> Some(mkTupledTy g argTys,delRetTy)
    | _ -> None

基準に沿っていることを確認したうえで、最初の引数を除いて返しています。そしてこれがdelegate制約の tuple-parameter-type と比較されるわけですね。

まとめ

  • IEventのような「標準のデリゲート型」の判定は型引数のdelegate制約によって行われている
  • delegate制約に指定するデリゲート引数の型は、最初のsenderを除いて、複数あればタプル型として指定する
  • 「標準のデリゲート型」の基準は、最初の引数が obj 型であり、残りの引数にref引数がないこと

第二引数はEventArgsを継承してなくてもいいんだ?

F#には Handler<'T> というデリゲートがあり、これは EventHandler<'T>'T がEventArgsじゃなくてもいい版です。

type Foo() =
    let event1 = new Event<int*int>()
    
    [<CLIEvent>]
    member this.Event1 = event1.Publish
    
    member this.Trigger(x, y) =
        event1.Trigger(x,y)

let foo = new Foo()
foo.Event1.Add(fun (x, y) -> printfn "%d,%d" x y)
foo.Event1.AddHandler <| new Handler<_>(fun sender (x, y) -> printfn "%d,%d" x y)
foo.Trigger(1,2)

タプルじゃなくて3引数以上のデリゲートでもいいんだよね?

DelegateEvent<'Delegate> を使えばそういうイベントも作れますし公開できます。

type FooDelegate = delegate of obj * int * int -> unit
type Foo() =
    let event1 = new DelegateEvent<FooDelegate>()
    
    [<CLIEvent>]
    member this.Event1 = event1.Publish
    
    member this.Trigger(x, y) =
        event1.Trigger([| obj(); x; y |])

let foo = new Foo()
foo.Event1.Add(fun (x, y) -> printfn "%d,%d" x y)
foo.Event1.AddHandler <| new FooDelegate(fun sender x y -> printfn "%d,%d" x y)
foo.Trigger(1,2)

でも第一引数にsenderは必須なんだ?

前述の TryDestStandardDelegateTyp の実装の近くにこんなコメントがあります。

In the F# design, we take advantage of the following idiom to simplify away the bogus "object" parameter of the of the "Add" methods associated with events. If you want to access it you can use AddHandler instead.

ハンドラ側でsenderを省略できるようにするうえで、デリゲート側には必ず存在するものとしておきたかった、というところなのかなと。