HTML+CSSで宛名印刷
あけましておめでとうございます。
近頃のCSSは writing-mode: vertical-rl
で縦書きができます。皆さんご存知の通り mm での位置指定は余裕ですし、 page-break-after
で改ページもできます。宛名印刷できますね。
というわけで今年の年賀状の宛名面のメイキングです。
なお下のQiita記事に触発され、土台部分を参考にしています。
レイアウト
基本となる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タグを並べるだけ。
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-type
が aty
、 return-type
が bty
に対応します。そしてこれらが対象の型である 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を省略できるようにするうえで、デリゲート側には必ず存在するものとしておきたかった、というところなのかなと。