この1~2週間くらい微妙に体調が悪い。咳とか。それ以外はそんなでもないけど、あまり活動的な気分になれない。
この1~2週間くらい微妙に体調が悪い。咳とか。それ以外はそんなでもないけど、あまり活動的な気分になれない。
KB2918614でインストールが遅くなった件ですが、10/13にWindows Installerに関する新しい更新プログラム KB3000988 が提供されたので、問題が解決されたか確認しました。
「コンポーネント登録を更新しています」の段階で長時間待たされ、その間インストールする全てのファイルを読み込んでおり、その後、ファイルをコピーする段階で再度全てのファイルを読み込むという、二度読みする挙動はそのまま変わっていませんでした。遅い。遅すぎる。どうするんでしょうね、これ。
一通り見終わった、かな?
うーん、パッとしない。
こういうことが簡単にできないのがJavaScriptの恐ろしいところですネ。従来は要素のoffsetParent, offsetLeft, offsetTop やgetBoundingClientRect なんかを使うのが定石でしたが(これもどうかと思うけど)、最近はCSS Transformなんてものがあるので困ってしまうわけです。CSSOM View Moduleに書いてある offsetLeft, offsetTop の説明にはtransformの影響を受けないと書かれています。影響を受けたところで、座標変換のためには伸縮や回転を考慮しなければならないので、話がややこしくなるだけでしょう。GeometryUtilsなんて面白そうなインタフェースが書かれていますが、今のところ中身は無し。何も決まっていないようです。これが出来れば将来的にはもっと楽に出来るようになるのかも?
WebKitの場合 webkitConvertPointFromPageToNode というそのものズバリの関数があるので、使える場合はおとなしくそれを使いましょう。Firefoxの場合MouseEventにlayerX,layerYというプロパティがあり、どうもそれが要素上の座標を示しているっぽいのですが、微妙にずれていたりしてよく分からないし、対応しているのはMozilla系だけなので無視します。
プラットフォーム側でサポートが無いのであれば、自分で行列を作って変換するのが良さそうです。transformプロパティには結構色々なことが書けるのですが、それもきちんと解析しないといけません。webkitCSSMatrixが他のブラウザでも使えるようになれば少しは楽になるかもしれませんが、まぁ、そんなことを言っていても仕方ないです。
まずはおさらいから。offsetParent,offsetLeft,offsetTopを使うとページ左上からの位置をたどることが出来ます。
offsetParentはparentNodeとは色々と異なります。offsetLeft,offsetTopは単純に一つ上の親からの相対位置にはならないんですね。なので、一工夫して一つ上の親からの相対位置に分解します。親と子でoffsetParentが同じなら子のoffsetLeftから親のoffsetLeftを引けば、親から子への相対位置が求められます。ただしoffsetParentがnullのときはルート、body要素、position:fixedな要素のいずれかです(Firefoxはposition:fixedでnullを返さないバグあり)ので、それ以上親を考慮する必要はありません。offsetParentがparentNodeと一致する場合は、親=offsetParentなので、offsetLeftがそのまま親からの相対位置になります。それらを考慮して親からの相対位置を求める関数relPosX(elem),relPosY(elem)を作ってみました。
function relPosX(elem){ return !elem.offsetParent ? elem.offsetLeft : elem.parentNode == elem.offsetParent ? elem.offsetLeft : elem.parentNode.offsetParent == elem.offsetParent ? elem.offsetLeft - elem.parentNode.offsetLeft : 0; } function relPosY(elem){ return !elem.offsetParent ? elem.offsetTop : elem.parentNode == elem.offsetParent ? elem.offsetTop : elem.parentNode.offsetParent == elem.offsetParent ? elem.offsetTop - elem.parentNode.offsetTop : 0; }
この関数が返す値はtransformを考慮しません。なので、完全な相対位置を求めるには、transformプロパティとtransform-originプロパティを解析する必要があります(3D変形は今回は対応しません)。
transform プロパティの文字列を解析して変換行列を返す関数parseCSSTransformを作りました。
function parseCSSTransform(tformStr) { //http://www.w3.org/TR/css3-transforms/#transform-property var funStrs = tformStr.split(")"); funStrs.pop(); var mat = matNewIdentity(); for(var i = 0; i < funStrs.length; ++i){ var funNameValue = funStrs[i].split("("); var fun = funNameValue[0].trim(); var args = funNameValue[1]; switch(fun){ // 2D Transform Functions // http://www.w3.org/TR/css3-transforms/#two-d-transform-functions case "matrix": matMul(mat, mat, matNewMatrix3x2.apply(null, args.split(","))); break; case "translate": var xy = args.split(","); matMul(mat, mat, matNewTranslate(parseFloat(xy[0]), parseFloat(xy[1] || "0"))); break; case "translateX": matMul(mat, mat, matNewTranslate(parseFloat(args), 0)); break; case "translateY": matMul(mat, mat, matNewTranslate(0, parseFloat(args))); break; case "scale": var sxy = args.split(","); matMul(mat, mat, matNewScale(parseFloat(sxy[0]), parseFloat(sxy.length >= 2 ? sxy[1] : sxy[0]))); break; case "scaleX": matMul(mat, mat, matNewScale(parseFloat(args), 1)); break; case "scaleY": matMul(mat, mat, matNewScale(1, parseFloat(args))); break; case "rotate": matMul(mat, mat, matNewRotate2D(toRadian(args))); break; case "skew": var wxy = args.split(","); matMul(mat, mat, matNewSkew2D(toRadian(wxy[0]), toRadian(wxy[1] || "0"))); break; case "skewX": matMul(mat, mat, matNewSkew2D(toRadian(args), 0)); break; case "skewY": matMul(mat, mat, matNewSkew2D(0, toRadian(args))); break; //http://www.w3.org/TR/css3-transforms/#three-d-transform-functions ///@todo support 3D Transform Functions } } return mat; } function toRadian(angle) { return angle.indexOf("deg") >= 0 ? parseFloat(angle) * (Math.PI/180) : angle.indexOf("grad") >= 0 ? parseFloat(angle) * (Math.PI/200) : parseFloat(angle); }
一緒に簡単な行列ライブラリ(matで始まる関数)も作成しています。少し長くなるので別途ソース参照のこと。
transform-origin プロパティの文字列を解析して座標を返す関数parseCSSTransformOriginを作りました。
function parseCSSTransformOrigin(originStr, boxSizes) { //http://www.w3.org/TR/css3-transforms/#transform-origin-property var values = originStr.split(" "); var origins = []; for(var i = 0; i < values.length; ++i){ var value = values[i].trim().toLowerCase(); if(value == ""){ continue; } var refSize = boxSizes[origins.length] || 0; switch(value){ case "left": origins.push(0); break; case "top": origins.push(0); break; case "right": origins.push(boxSizes[0] || 0); break; case "bottom": origins.push(boxSizes[1] || 0); break; case "center": origins.push(refSize*0.5); break; default: if(value.indexOf("%") > 0){ origins.push(refSize * parseFloat(value) / 100); } else{ origins.push(parseFloat(value)); } break; } } while(origins.length < 2){ refSize = boxSizes[origins.length] || 0; origins.push(refSize * 0.5); //default 50% } return origins; }
この二つの関数が出来ると、要素に設定されているCSS Transformの影響を変換行列の形で求めることが出来ます。
function getElementComputedStyle(elem) { return window.getComputedStyle ? window.getComputedStyle(elem, "") : document.defaultView && document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(elem, "") : elem.currentStyle || elem.style; } /// CSS Transformのプロパティ(transformプロパティやtransform-originプロパティなど)による変換行列を求めます。 function getCSSTransformMatrix(elem) { var elemStyle = getElementComputedStyle(elem); var origins = parseCSSTransformOrigin(elemStyle.transformOrigin, [elem.offsetWidth, elem.offsetHeight]); var tform = parseCSSTransform(elemStyle.transform); return matNewMul(matNewTranslate(origins[0], origins[1]), matNewMul(tform, matNewTranslate(-origins[0], -origins[1]))); }
transform-originで指定される原点を引いて、transformを適用し、原点を足して戻すという変換になります。
プロパティの値はできるだけ計算値から求めるようにしています。(transformの計算値は常にmatrixだけのようなので、parseCSSTransformを律儀に作成しなくても良かった気もしますが、まぁ、他でも使えるかもしれないのでいいや)
親子の相対位置とCSS Transformによる変形行列を合成して、どんどんルートまで合成していくと、最終的に要素座標からページ座標へ変換する行列が完成します。
/// 要素内bounding-box左上を原点とする座標系からページ左上を原点とする座標系へ変換する行列を求めます。 function getMatrixFromNodeToPage(elem) { var mat = matNewIdentity(); while(elem){ var tformMat = getCSSTransformMatrix(elem); if(tformMat){ matMul(mat, tformMat, mat); } matMul(mat, matNewTranslate(relPosX(elem), relPosY(elem)), mat ); var elemStyle = getElementComputedStyle(elem); if(elemStyle && elemStyle.position == "fixed"){ //see Firefox Bug https://bugzilla.mozilla.org/show_bug.cgi?id=434678 matMul(mat, matNewTranslate(window.pageXOffset, window.pageYOffset), mat); break; } if(!elem.offsetParent){ break; //root or body } elem = elem.parentNode; } return mat; }
position:fixedの場合、スクロールしてもoffsetLeft, offsetTopの値は一切変わらないみたいなのですが、MouseEventのpageX, pageYは当然スクロールに応じて大きな値になるので、fixedの時だけwindow.pageXOffset,window.pageYOffsetを足すことにしています。
後は逆行列を作れば、ページ座標から要素座標へ変換できるようになります。MouseEventのpageX,pageYから要素上の座標へ変換できます。
function convertPointFromPageToNode(elem, pageX, pageY) { if(global.webkitConvertPointFromPageToNode && global.WebKitPoint){ // webkitConvertPointFromPageToNodeが使える場合はそれを使う! var r = webkitConvertPointFromPageToNode(elem, new WebKitPoint(pageX, pageY)); return [r.x, r.y]; } else{ return convertPointFromPageToNode_Impl(elem, pageX, pageY); } } function convertPointFromPageToNode_Impl(elem, pageX, pageY) { var matPageToElement = matNewInverse(getMatrixFromNodeToPage(elem)); if(matPageToElement){ return matNewMulVec(matPageToElement, [pageX, pageY]); } else{ return [0,0]; } }
この方法で得られるのは境界矩形(bounding-box)左上を原点とする座標系の位置です。つまり、paddingとborderを含む領域の左上が(0,0)になります。この座標系はcanvas要素で使うには不便です。コンテンツ領域左上を原点とする座標系に変換したいです。なので、padding-left, padding-top, border-left-width, border-top-widthの計算値を求めて、少しずらしてやります。
/// 要素コンテンツ領域左上側の幅と高さを求めます。 function getElementOuterLeftTop(elem) { var elemStyle = getElementComputedStyle(elem); function getNumber(propName){ return parseFloat(elemStyle.getPropertyValue(propName));} var left = getNumber("border-left-width") + getNumber("padding-left") || 0; var top = getNumber("border-top-width") + getNumber("padding-top") || 0; return [left, top]; } // ページ座標から要素内コンテンツ領域左上を原点とする座標系へ変換します。 function convertPointFromPageToNodeContentArea(elem, pageX, pageY) { var outerLeftTop = getElementOuterLeftTop(elem); var pos = convertPointFromPageToNode(elem, pageX, pageY); pos[0] -= outerLeftTop[0]; pos[1] -= outerLeftTop[1]; return pos; }
まとめ:
参考URL:
JavaScriptで作られたゲームみたいなコンテンツをhtmlに埋め込むときに便利なライブラリを作りました。
http://github.com/misohena/js_gamescreen/
ゲームなどのコンテンツをWebページに貼り付けるとき、Youtube動画を埋め込むみたいにプレイボタンを押してから開始するようにしたくありませんか? プレビュー(サムネイル)画像を用意するのは仕方ないとして、いちいちプレイボタン画像を用意するのは面倒ですよね? ページを軽くするためにJavaScriptはボタンを押してから読み込むようにしたいですよね。読み込み終わるまでグルグルアニメーションしたいですよね。必要な.jsを読み込み終わったらプレビュー用の要素と実際のコンテンツの要素を交換したいですよね。そんな場合にgameboot.jsを使います。
埋め込んだコンテンツをウィンドウ内で最大化表示したり、全画面化(フルスクリーン化)したりしたいですよね? ゲームには色々UIが必要になってきますが、簡単なゲームだと出来合いのメニューバーみたいなものがあるとなんだかんだ言って楽ですよね。Youtubeの画面下部にあるようなコントロールバーがあれば便利ですよね? そんな場合にgamescreen.jsを使います。
(キーボード←,→,スペースを使用。フォーカス注意)
台風19号が三連休最後の日に通り過ぎた後、ずいぶん涼しくなってきました。そろそろ冬の準備をしないといけませんね。今が一番快適に過ごせる季節なので、存分に楽しみたいところでもあります。
インストーラの話題をしたところなので、WiX ToolsetでWindows Installer(.msi)を作るためのMyテンプレートなどを一つ。
ディレクトリ構成は次のような感じで。
WiX Toolsetをダウンロードしてパスを通し、上のディレクトリ構成でbuild.batを実行するとoutput/DISK1の中にインストーラが出力されます。
ビルドするためのバッチファイルです。 出力ディレクトリの作成、wxsのコンパイル、wixobjのリンクを行います。
mkdir output mkdir output\DISK1 candle hoge.wxs -o output/hoge.wixobj candle MyExitDialog.wxs -o output/MyExitDialog.wixobj candle MyInstallDirDlg.wxs -o output/MyInstallDirDlg.wixobj candle MyUI_InstallDir.wxs -o output/MyUI_InstallDir.wixobj light output/hoge.wixobj ^ output/MyExitDialog.wixobj ^ output/MyInstallDirDlg.wixobj ^ output/MyUI_InstallDir.wixobj ^ -ext WixUIExtension ^ -ext WixUtilExtension ^ -o output/DISK1/hoge.msi ^ -pdbout output/hoge.wixpdb ^ -cultures:ja-jp
メインとなるソースファイルです。
{YOUR-GUID}のところは全て個別のGUIDを生成して置き換えてください。
ファイル先頭ではプリプロセッサ変数を定義しています。この変数の値によって、生成されるmsiが色々変わるようになってます。
<!– Files –>と書いてある部分はheatで生成しても良いと思います。
<?xml version="1.0" encoding="utf-8"?> <?define ProductName = "製品名" ?> <?define Manufacturer = "製造者" ?> <?define SrcDir = "$(sys.CURRENTDIR)src\" ?> <?define MainExeFileName = "hoge.exe" ?> <?define ReadmeFileName = "readme.txt" ?> <?define UseCustomUI = "yes" ?> <?define AppRegKey = "Software\$(var.Manufacturer)\$(var.ProductName)" ?> <?define Compressed = "no" ?> <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> <Product Id="{YOUR-GUID}" UpgradeCode="{YOUR-GUID}" Name="$(var.ProductName)" Manufacturer="$(var.Manufacturer)" Version="1.0.0.0" Language="1041" Codepage="932"> <Package InstallerVersion="200" Compressed="$(var.Compressed)" /> <?if $(var.Compressed)="yes" ?> <MediaTemplate EmbedCab="yes" /> <?else ?> <Media Id="1" DiskPrompt="DISK1" /> <Property Id="DiskPrompt" Value="インストールディスク [1]" /> <?endif ?> <Icon Id="MainIcon" SourceFile="$(var.SrcDir)$(var.MainExeFileName)" /> <Property Id="ARPPRODUCTICON" Value="MainIcon" /> <!-- Directory Structure --> <Directory Id="TARGETDIR" Name="SourceDir"> <!-- INSTALLDIR --> <Directory Id="ProgramFilesFolder" Name="ProgramFiles"> <Directory Id="ProgramFilesManufacturer" Name="$(var.Manufacturer)"> <Directory Id="INSTALLDIR" Name="$(var.ProductName)"> </Directory> </Directory> </Directory> <!-- Start Menu --> <Directory Id="ProgramMenuFolder" Name="Programs"> <Directory Id="AppStartMenuDir" Name="$(var.Manufacturer) $(var.ProductName)"> </Directory> </Directory> <!-- Desktop --> <Directory Id="DesktopFolder" Name="Desktop"> </Directory> </Directory> <!-- Feature --> <Feature Id="EssentialFeature" Level="1"> <ComponentGroupRef Id="EssentialFiles" /> <ComponentRef Id="EssentialRegistries" /> <ComponentRef Id="EssentialShortcuts" /> <ComponentRef Id="EssentialDesktopShortcut" /> </Feature> <!-- Files --> <ComponentGroup Id="EssentialFiles" Directory="INSTALLDIR"> <Component Id="file0001" Guid="{YOUR-GUID}"> <File Id="file0001" KeyPath="yes" Source="$(var.SrcDir)$(var.MainExeFileName)" /> </Component> <Component Id="file0002" Guid="{YOUR-GUID}"> <File Id="file0002" KeyPath="yes" Source="$(var.SrcDir)hoge.dat" /> </Component> <Component Id="file0003" Guid="{YOUR-GUID}"> <File Id="file0003" KeyPath="yes" Source="$(var.SrcDir)$(var.ReadmeFileName)" /> </Component> </ComponentGroup> <!-- Shortcuts --> <DirectoryRef Id="AppStartMenuDir"> <Component Id="EssentialShortcuts" Guid="{YOUR-GUID}"> <RegistryValue Root="HKCU" Key="$(var.AppRegKey)" Name="InstalledStartMenuShortcut" Type="integer" Value="1" KeyPath="yes" /> <RemoveFolder Id="AppStartMenuDir" On="uninstall" /> <Shortcut Id="startMenuShortcut0001" Name="$(var.ProductName)" Target="[INSTALLDIR]$(var.MainExeFileName)" WorkingDirectory="INSTALLDIR" /> </Component> </DirectoryRef> <Property Id="INSTALLDESKTOPSHORTCUT" Value="1" /> <DirectoryRef Id="DesktopFolder"> <Component Id="EssentialDesktopShortcut" Guid="{YOUR-GUID}"> <Condition>INSTALLDESKTOPSHORTCUT</Condition> <RegistryValue Root="HKCU" Key="$(var.AppRegKey)" Name="InstalledDesktopShortcut" Type="integer" Value="1" KeyPath="yes" /> <Shortcut Id="desktopShortcut0001" Name="$(var.ProductName)" Target="[INSTALLDIR]$(var.MainExeFileName)" WorkingDirectory="INSTALLDIR" /> </Component> </DirectoryRef> <!-- Registries --> <DirectoryRef Id="INSTALLDIR"> <Component Id="EssentialRegistries" Guid="{YOUR-GUID}"> <RegistryKey Root="HKCU" Key="$(var.AppRegKey)" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes"> <RegistryValue Type="string" Name="InstalledPath" Value="[INSTALLDIR]" KeyPath="yes" /> </RegistryKey> </Component> </DirectoryRef> <!-- User Interface --> <!-- UI: Install Directory --> <Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" /> <!-- UI: Bitmap --> <WixVariable Id="WixUIBannerBmp" Value="banner.bmp" /><!-- 493x58 --> <WixVariable Id="WixUIDialogBmp" Value="dialog.bmp" /><!-- 493x312 --> <?if $(var.UseCustomUI) = "yes" ?> <UIRef Id="MyUI_InstallDir" /> <!-- UI: Run application after installed (for MyUI_InstallDir) --> <?if $(var.MainExeFileName) And $(var.MainExeFileName)!="" ?> <Property Id="MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="プログラムを実行する" /> <Property Id="MYUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" /> <CustomAction Id="LaunchApplication" Directory="INSTALLDIR" ExeCommand="[INSTALLDIR]$(var.MainExeFileName)" Return="asyncNoWait" /> <UI> <Publish Dialog="MyExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">MYUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish> </UI> <?endif ?> <?if $(var.ReadmeFileName) And $(var.ReadmeFileName)!="" ?> <Property Id="MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT2" Value="Readmeを開く" /> <Property Id="MYUI_EXITDIALOGOPTIONALCHECKBOX2" Value="1" /> <Property Id="WixShellExecTarget" Value="[INSTALLDIR]$(var.ReadmeFileName)" /> <CustomAction Id="LaunchReadme" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" /> <UI> <Publish Dialog="MyExitDialog" Control="Finish" Event="DoAction" Value="LaunchReadme">MYUI_EXITDIALOGOPTIONALCHECKBOX2 = 1 and NOT Installed</Publish> </UI> <?endif ?> <!-- UI: Skip license (for MyUI_InstallDir) --> <UI> <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg">1</Publish> <Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish> </UI> <?else ?> <UIRef Id="WixUI_InstallDir" /> <!-- UI: Run application after installed (for WixUI_InstallDir) --> <Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="プログラムを実行する" /> <Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" /> <CustomAction Id="LaunchApplication" Directory="INSTALLDIR" ExeCommand="[INSTALLDIR]$(var.MainExeFileName)" Return="asyncNoWait" /> <UI> <Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish> </UI> <!-- UI: Skip license (for WixUI_InstallDir) --> <UI> <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">1</Publish> <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish> </UI> <?endif ?> </Product> </Wix>
インストール先を指定するダイアログを定義するファイルです。
WiXのソースに含まれているファイル(src/ext/UIExtension/InstallDirDlg.wxs)をコピーして一部を改編しました。改編点は次の通りです。
<?xml version="1.0" encoding="UTF-8"?> <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> <Fragment> <UI> <Dialog Id="MyInstallDirDlg" Width="370" Height="270" Title="!(loc.InstallDirDlg_Title)"> <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)" /> <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" /> <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)"> <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish> </Control> <Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.InstallDirDlgDescription)" /> <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.InstallDirDlgTitle)" /> <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.InstallDirDlgBannerBitmap)" /> <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" /> <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" /> <Control Id="FolderLabel" Type="Text" X="20" Y="60" Width="290" Height="30" NoPrefix="yes" Text="!(loc.InstallDirDlgFolderLabel)" /> <Control Id="Folder" Type="PathEdit" X="20" Y="100" Width="320" Height="18" Property="WIXUI_INSTALLDIR" Indirect="yes" /> <Control Id="ChangeFolder" Type="PushButton" X="20" Y="120" Width="56" Height="17" Text="!(loc.InstallDirDlgChange)" /> <!-- Begin MyUI --> <Control Id="DesktopShortcutCheckBox" Type="CheckBox" X="20" Y="160" Width="290" Height="17" Property="INSTALLDESKTOPSHORTCUT" CheckBoxValue="1" Text="デスクトップにショートカットを作成する。" /> <!-- End MyUI --> </Dialog> </UI> </Fragment> </Wix>
インストールが終了したときに表示するダイアログを定義するファイルです。
WiXのソースに含まれているファイル(src/ext/UIExtension/ExitDialog.wxs)をコピーして一部を改編しました。改編点は次の通りです。
<?xml version="1.0" encoding="UTF-8"?> <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> <Fragment> <UI> <Dialog Id="MyExitDialog" Width="370" Height="270" Title="!(loc.ExitDialog_Title)"> <Control Id="Finish" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Cancel="yes" Text="!(loc.WixUIFinish)" /> <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Disabled="yes" Text="!(loc.WixUICancel)" /> <Control Id="Bitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="234" TabSkip="no" Text="!(loc.ExitDialogBitmap)" /> <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Disabled="yes" Text="!(loc.WixUIBack)" /> <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" /> <Control Id="Description" Type="Text" X="135" Y="70" Width="220" Height="40" Transparent="yes" NoPrefix="yes" Text="!(loc.ExitDialogDescription)" /> <Control Id="Title" Type="Text" X="135" Y="20" Width="220" Height="60" Transparent="yes" NoPrefix="yes" Text="!(loc.ExitDialogTitle)" /> <!-- Begin MyUI --> <Control Id="OptionalText" Type="Text" X="135" Y="110" Width="220" Height="80" Transparent="yes" NoPrefix="yes" Hidden="yes" Text="[MYUI_EXITDIALOGOPTIONALTEXT]"> <Condition Action="show">MYUI_EXITDIALOGOPTIONALTEXT AND NOT Installed</Condition> </Control> <Control Id="OptionalCheckBox" Type="CheckBox" X="150" Y="150" Width="10" Height="9" Hidden="yes" Property="MYUI_EXITDIALOGOPTIONALCHECKBOX" CheckBoxValue="1"> <Condition Action="show">MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT AND NOT Installed</Condition> </Control> <Control Id="OptionalCheckBoxText" Type="Text" X="165" Y="150" Width="200" Height="13" Transparent="yes" Hidden="yes" Text="[MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT]"> <Condition Action="show">MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT AND NOT Installed</Condition> </Control> <Control Id="OptionalCheckBox2" Type="CheckBox" X="150" Y="180" Width="10" Height="9" Hidden="yes" Property="MYUI_EXITDIALOGOPTIONALCHECKBOX2" CheckBoxValue="1"> <Condition Action="show">MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT2 AND NOT Installed</Condition> </Control> <Control Id="OptionalCheckText2" Type="Text" X="165" Y="180" Width="200" Height="13" Transparent="yes" Hidden="yes" Text="[MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT2]"> <Condition Action="show">MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT2 AND NOT Installed</Condition> </Control> <!-- End MyUI --> </Dialog> <InstallUISequence> <Show Dialog="MyExitDialog" OnExit="success" Overridable="yes" /> </InstallUISequence> <AdminUISequence> <Show Dialog="MyExitDialog" OnExit="success" Overridable="yes" /> </AdminUISequence> </UI> </Fragment> </Wix>
UIの流れを定義するファイルです。
WiXのソースに含まれているファイル(src/ext/UIExtension/WixUI_InstallDir.wxs)をコピーして一部を改編しました。上で改編したダイアログを使うように、一部のダイアログIdにMyを付加しました。
<?xml version="1.0" encoding="UTF-8"?> <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> <Fragment> <UI Id="MyUI_InstallDir"> <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" /> <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" /> <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" /> <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" /> <Property Id="WixUI_Mode" Value="InstallDir" /> <DialogRef Id="BrowseDlg" /> <DialogRef Id="DiskCostDlg" /> <DialogRef Id="ErrorDlg" /> <DialogRef Id="FatalError" /> <DialogRef Id="FilesInUse" /> <DialogRef Id="MsiRMFilesInUse" /> <DialogRef Id="PrepareDlg" /> <DialogRef Id="ProgressDlg" /> <DialogRef Id="ResumeDlg" /> <DialogRef Id="UserExit" /> <Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish> <Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]></Publish> <Publish Dialog="MyExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish> <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg">NOT Installed</Publish> <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish> <Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish> <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg">LicenseAccepted = "1"</Publish> <Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish> <Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish> <Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish> <Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish> <Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish> <Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish> <Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish> <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MyInstallDirDlg" Order="1">NOT Installed</Publish> <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed AND NOT PATCH</Publish> <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">Installed AND PATCH</Publish> <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish> <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish> <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish> <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish> <Property Id="ARPNOMODIFY" Value="1" /> </UI> <UIRef Id="WixUI_Common" /> </Fragment> </Wix>
ソース一式は github/simple_wix_template に置いてあります。
InstallShieldで作ったインストーラが信じられないくらい遅いので調べたところ、どうもセキュリティ更新プログラム KB2918614 に問題があるようなのです。
作ったインストーラはDVD-ROMから4GBくらいインストールするのですが、インストールを開始してすぐ、準備段階でプログレスバーが止まったまま10分程度まったく動かなくなります。それを過ぎるとファイルのコピーが始まり、最終的には正常にインストールが完了するのですが、以前はこんなこと起きなかったのにどうなっているんだろうと思い色々調べてみました。
タスクマネージャでmsiexec.exe(.msiを実際に実行するプロセス)のI/O読み取りバイト数を見てみると、止まっている間でもガンガン何かを読み込んでいることが分かります。4GBくらい読み込んだところでプログレスバーが動き始めコピーをしていると表示があり、また4GBくらい読み込むとインストールが終了します。インストールするファイル(もちろん無圧縮、アーカイブ無し)を計2回も読み込んでいるようなのです。そりゃ遅いはずです。それもそのうち最初の1回はプログレスバーが全く動きません。DVD-ROMから4GB読み込むのに10分くらいかかりますから、その間止まっているように見えるわけです。
試しにHDDにインストーラをコピーしてから実行したり、新しくWiX Toolsetでmsiを作ってみたりしましたが、やはりWindows Installerを使う限り二回読み込むことには変わりないようです。
同じインストーラの作り方で以前はこのようなことは起きませんでした。なので以前作ったインストーラを調べてみたのですが、今実行するととてつもなく遅い! 同じ問題が起きています。これはOS側、Windows Installer側に何かあったのでは無いか、と思いました。
Webで色々調べているとKB2918614という単語を見かけました。
【至急】KB2918614適用後、アプリケーションの画像データのインストールに非常に時間がかかるようになった - マイクロソフト コミュニティ
一般コンシューマ向けパッケージソフト製品開発を行っている企業で、開発者をしております。
8/13のWindows UpdateでKB2918614適用後、以下のようなコンポーネントのインストールに非常に時間がかかるようになってしまいました。
環境によっては数時間同じ画面で停止しているという状況になりますので、エンドユーザーから見るとインストールが停止したような形になります。
おそらくは問い合わせが多数寄せられると思いますので、まずはユーザーへの対応策を検討する必要があります。
..略…
膨大なロットを市場に出荷しており、発売直後に第1の売り上げのピークを記録するというソフトウェア製品の性格上、
対応次第ではかなりの損害が出てしまいかねない状況ですので、大至急ご確認をお願いいたします。
なかなか生々しいですね……。
KB2918614はMS14-049の脆弱性を解決するためのセキュリティ更新プログラムのようです。出たのは今年の8月。
筆まめやウィルスバスターなど、いくつかの製品のサポート情報では対策としてKB2918614のアンインストールを挙げています。
その他Twitterを検索してみると8月にKB2918614が出て以降、様々なアプリケーションがインストール不能に陥っているようですね。
試しにKB2918614をアンインストールして再度インストーラの動きを確認してみました。するとすぐにコピーが始まりますし、msiexec.exeは1回分(4GB)しか読み込みません。再度KB2918614を入れると遅くなります。KB2918614の有無で動作が変わるのは間違いないようです。
msiexec /L*v log.txt /i hoge.msi
としてログを取ってみましたが、KB2918614が入っているときは止まって見える間に SECREPAIR: CryptAcquireContext succeeded
のようにSECREPAIRという単語を含むログが出力されます。KB2918614が入っていないときは出力されません。セキュリティ上の確認処理か何かをしているのでしょうか。
結局対策としては、
ということですね。
セキュリティ更新プログラムなのでアンインストールするとセキュリティ上の問題MS14-049が残ってしまうので注意が必要だと思います。
KB2918614で挙げられている既知の問題について、マイクロソフトは現在調査中としています。「遅い」というのは既知の問題として挙げられていませんが、近いうちに修正されることを祈るしか無いでしょう。修正されなければWindows Installerは死んだも同然でしょうね。プログレスバーが数十分も止まったままになるインストーラなんて使い物になりません。msiではない古き良きスクリプト駆動型インストーラに戻るしかありません。
ちょうど番組が終わる頃に洗脳が完了して結局買ってしまうのはいつものことですね。
Org2blogの設定をしました。
下のelispで次のようなことをします。
;; ブログorgファイルのセーブ先 (setq my-blog-directory "~/org/blog/") ;; 投稿設定 (setq org2blog/wp-blog-alist `(("example" :url "http://example.jp/xmlrpc.php" :username "example-user" :default-title "NewEntry" :default-categories nil ;カテゴリーはデフォルト :track-posts (,(concat my-blog-directory ".org2blog.org") "Posts") ;.org2blogの保存先を変える ))) (setq org2blog/wp-default-categories '()) ;カテゴリーは使わないので空 (setq org2blog/wp-buffer-template "#+DATE: %s #+OPTIONS: toc:nil num:nil todo:nil pri:nil tags:nil ^:nil #+CATEGORY: %s #+TAGS: #+PERMALINK: #+TITLE: %s \n") ;必ずPERMALINKを入れる ;; セーブ時のファイル名生成 (defun my-blog-get-buffer-post-file-name () "現在のバッファのファイル名を作成します。directory/YYYY-MM-DD-permalink_or_title.orgの形式です。directoryはmy-blog-directory変数を使います。" (let* ((date (org2blog/wp-get-option "DATE")) (title (org2blog/wp-get-option "TITLE")) (permalink (org2blog/wp-get-option "PERMALINK")) (filename-date (format-time-string "%Y-%m-%d" (if date (apply #'encode-time (org-parse-time-string date)) (current-time))))) (concat my-blog-directory filename-date "-" (if (> (length permalink) 0) permalink title) ".org"))) (defun my-blog-set-buffer-file-name () "デフォルトのファイル名をバッファに設定します。" (if (not (buffer-file-name)) (let ((filename (my-blog-get-buffer-post-file-name))) (if (y-or-n-p (format "set filename to '%s'?" filename)) (set-visited-file-name filename))))) (defun my-blog-save () "バッファをセーブします。まだバッファにファイル名が設定されていないとき、セーブする前にデフォルトのファイル名を設定するかどうかを訪ねます。" (interactive) (my-blog-set-buffer-file-name) (save-buffer)) (defun blog-new () "ブログの新しいエントリーを作成します。" (interactive) (org2blog/wp-new-entry) (local-set-key "\C-x\C-s" 'my-blog-save)) ;; ブログディレクトリ下のファイルを開くときはorg2blogを有効にする。 (add-hook 'org-mode-hook (lambda () (if (and (buffer-file-name) (string-prefix-p (expand-file-name my-blog-directory) (buffer-file-name))) (org2blog/wp-mode t))))
最初はorg2blog/wp-kill-buffer-hookをdefadviceで書き換えたりしたのですが、いくつかのケースでうまく動かなかったためやめておきました。元々少し不具合があるみたいですし、ちゃんとやるならもっと色々手を入れないとダメそう。