three.jsで多色グラデーション(その2)

three.jsで多色グラデーションの2回目。

前回の記事ではShaderMaterialを使った裏技グラデーションでしたが、今回は正攻法です。
以下は全ソース(html)です。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>shade test</title>
  <script type="text/javascript" src="three.min.js"></script>
  <script type="text/javascript" src="OrbitControls.js"></script>
  <script type="text/javascript">
    var renderer;
    var camera;
    var controls;
    var scene;
    var container;
    var light;
    var light2;
    var mesh;
    var geometry;
    function shadetest() {
      init_threejs();
      init_scene();
      init_camera();
      init_light();
      init_object();
      reset_position();
      loop();
    }
    function init_threejs() {
      renderer = new THREE.WebGLRenderer({antialias: true});
      renderer.setClearColor(0xffffff, 1);
      renderer.setSize(1, 1);
      container = document.getElementById('canvas3d');
      container.appendChild(renderer.domElement);
    }
    function init_scene() {
      scene = new THREE.Scene();
    }
    function init_camera() {
      camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100000);
      camera.up.x = 0;
      camera.up.y = 0;
      camera.up.z = 1;
      camera.position.set(0, 0, 5);
      controls = new THREE.OrbitControls(camera, renderer.domElement);
    }
    function init_light() {
      light = new THREE.DirectionalLight(0xcccccc, 1.6);
      light.position = new THREE.Vector3(-100, 500, 800);
      scene.add(light);
      light2 = new THREE.AmbientLight(0x333333);
      scene.add(light2);
    }
    function init_object() {
      var vertexPositions = [
        [-1.0, -1.0, 1.0],
        [0.0, -1.0, 1.0],
        [1.0, -1.0, 1.0],
        [-1.0, 0.0, 1.0],
        [0.0, 0.0, 1.0],
        [1.0, 0.0, 1.0],
        [-1.0, 1.0, 1.0],
        [0.0, 1.0, 1.0],
        [1.0, 1.0, 1.0]
      ];
      var vertexColors = [
        [1.0, 0.0, 0.0],
        [0.0, 1.0, 0.0],
        [0.0, 0.0, 1.0],
        [1.0, 1.0, 0.0],
        [0.0, 1.0, 1.0],
        [1.0, 0.0, 1.0],
        [0.5, 0.5, 0.0],
        [0.0, 0.5, 0.5],
        [0.5, 0.0, 0.5]
      ];
      var indices = [
        [0, 1, 4],
        [1, 2, 4],
        [2, 5, 4],
        [5, 8, 4],
        [8, 7, 4],
        [7, 6, 4],
        [6, 3, 4],
        [3, 0, 4]
      ];
      var geometry = new THREE.Geometry();
      for (var i = 0; i < vertexPositions.length; i++) {
        geometry.vertices.push(new THREE.Vector3(vertexPositions[i][0], vertexPositions[i][1], vertexPositions[i][2]));
      }
      for (var i = 0; i < indices.length; i++) {
        var face = new THREE.Face3(indices[i][0], indices[i][1], indices[i][2]);
        for (var j = 0; j < 3; j++) {
          var idx = indices[i][j];
          face.vertexColors[j] = new THREE.Color(vertexColors[idx][0], vertexColors[idx][1], vertexColors[idx][2]);
        }
        geometry.faces.push(face);
      }
      material = new THREE.MeshBasicMaterial({vertexColors: THREE.FaceColors});
      var mesh = new THREE.Mesh(geometry, material);
      mesh.position.set(0, 0, 0);
      scene.add(mesh);
    }
    function reset_position() {
      var sz = {w:container.offsetWidth, h:container.offsetHeight};
      renderer.setSize(sz.w, sz.h);
      camera.aspect = sz.w / sz.h;
      camera.updateProjectionMatrix();
    }
    function loop() {
      requestAnimationFrame(loop);
      controls.update();
      renderer.clear();
      renderer.render(scene, camera);
    }
  </script>
</head>
<body onload="shadetest();" onresize="reset_position();">
  <div id="canvas3d" style="width:300px;height:300px;">
  </div>
</body>
</html>

faceの頂点(vertexColors)にそれぞれ異なる色を設定するとfaceが多色グラデーションとなる。
face.colorが設定されていてもvertexColorsを優先するが、face.colorを優先したいなら、

face.vertexColors = [];

とすればOKだ。

Ajax通信でブラウザの「戻る」ボタンを使う

Ajax通信は便利だが、厄介な問題を抱えている。
ブラウザの「戻る」ボタンが使えないのだ。
Historyが残らないのである。
これはかなりマズイ。
例えば割と時間をかけて取得した画面を表示した後、一旦別の画面に遷移したが、また直前の(時間をかけて取得した)画面へ戻りたい時、ブラウザの「戻る」で直前の履歴に戻れなければ、もう一度時間をかけて画面の情報を取得しなければならない。
これではユーザから必ずクレームが来る。
ところがjavascriptにこれを解決する手段が無いのだ。
(ハック的な実現方法はあるらしいが、使わないほうが無難だ。)
途方に暮れていると、html5でまさにこれを解決する手段が提供されていた。
history.pushStateである。
早速使ってみよう。

その前にこの関数のcallingシーケンスは以下だ。

history.pushState(stateObj, title, url);
stateObj: 履歴エントリに関連付けられるJavaScriptオブジェクト
title: 将来的拡張(現在未使用です。)
url: 履歴エントリのURL

以下はサンプルだ。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="./jquery-1.7.2.min.js"></script>
  <title>javascript test</title>
  <script type="text/javascript">
    var doajax = function(id, url) {
      document.body.style.cursor = 'wait';
      $.ajax({
        type: "get",
        url: url,
        dataType: "text",
        success: function(res){
          $(id).html(res);
          if(window.history && window.history.pushState) {
            history.pushState({response: res, id: id}, null);
          }
        },
        complete: function(xhr, textStatus) {
          document.body.style.cursor = 'auto';
        },
        error: function(xhr, textStatus, error) {
          console.log('error...: ' + textStatus + ' :' + error);
        }
      });
    }
    window.addEventListener(
      "popstate",
      function(event) {
        if(event.state != null) {
          $(event.state.id).html(event.state.response);
        } else {
          $("#target").html("hello, history");
        }
      }
    );
  </script>
</head>
<body>
  <div id="target">hello, history</div>
  <input type="button" value="go 1!" onClick="doajax('#target', 'http://localhost/1.txt')"><br />
  <input type="button" value="go 2!" onClick="doajax('#target', 'http://localhost/2.txt')"><br />
  <input type="button" value="go 3!" onClick="doajax('#target', 'http://localhost/3.txt')"><br />
</body>
</html>

DIV要素に表示された「hello, history」を「go 1!」、「go 2!」、「go 3!」ボタンで書き換える。
書き換えはajaxで行う。
取得するデータは単なるtextファイルだ。
local環境に1.txt、2.txt、3.txtを用意する。
ajaxのコールバックsuccessでpushStateを使う。

if(window.history && window.history.pushState) {
  history.pushState({response: res, id: id}, null);
}

最初のif文はpushStateの実装のチェックだ。
history.pushStateの第一引数にsuccessのレスポンス(1.txt or 2.txt or 3.txtの内容)と更新する要素のidを持つオブジェクトを指定する。第3引数は省略している。
(省略するとブラウザのURLは変わらない。指定すればこの内容がURLに表示される。)
このオブジェクトはaddEventListenerで登録する”popstate”コールバックで送られてくる。
“popstate”コールバックはブラウザの「戻る」「進む」ボタン押下時に呼び出される。
このタイミングでajaxのsuccessと同様の処理を行う。

window.addEventListener(
  "popstate",
  function(event) {
    if(event.state != null) {
      $(event.state.id).html(event.state.response);
    } else {
      $("#target").html("hello, history");
    }
  }
);

event.stateがnullでなければ履歴が存在するので、event.stateの内容(successのレスポンスと更新対象の要素ID)でDOMを更新する。event.stateがnullであれば履歴が無いので初期状態に戻す。
これで履歴が再現できる。

html5に感謝!

LinuxでWebGL動作せず

three.jsで作った3Dをlinuxで動作させたところ下記エラーが発生。

THREE.WebGLRenderer: Error creating WebGL context.

Windowsでは動作したのに…。

開発環境は以下。
Electron: v1.4.13
node.js: v4.4.4
three.js: 0.83.0
linux: Ubuntu14.04(64bit)
VM: Hyper-V

Electronは内部にChromeエンジンを搭載しているとのことで、まずはChromeを疑ってみる。
node.jsの影響が無いことを確認するために、前掲three.jsで多色グラデーションを試してみる。

まずはFirefoxで…。
これは問題なく動作する。

次にChrome最新版(57.0.2987.133-64bit)。
むむ…、動作せず。
デベロッパーツールで確認。

THREE.WebGLRenderer: Error creating WebGL context.

同じエラーである。
やはりChromeの問題であった。
ぐぐってみると、ChromeはWebGLのサポートを停止している模様。
[WebGL][Chrome]Chrome 10からWindows XPでのWebGLサポートを停止
でもWindowsでは動作したのに、何故linuxだけなのだろう…。
(因みにCentOS6でも動作せず。)

回避するにはchrome://flagsで「ソフトウェアレンダリングリストをオーバライド」を有効にする、といいうのが定石らしい。

これでChromeのWebGLは動作した。
あとはElectronである。

これがなかなかわからなかったのですが、ヒントはchrome://flags「ソフトウェアレンダリングリストをオーバライド」のハッシュ(#)にありました。

このフラグのハッシュ名は「ignore-gpu-blacklist」である。
なんかちょっと引く名前だ。
VMのgpuがブラックリストに載っているんでしょうか…?
ちょっと気持ち悪いんですが、これをElectronで指定できそうです。

const {app} = require('electron');
app.commandLine.appendSwitch('ignore-gpu-blacklist');

readyイベントの前に実行しなければならないので、スコープ外に記述します。

これでElectron(Ubuntu)でも無事Three.jsが動作しました。

Electronのipc通信で連想配列を送れるか?

Electronではレンダーとサーバーは別プロセスなので、情報交換はプロセス間通信となる。
そんな訳で情報交換には多少の制約はありそうだ。
以下の例はプロセス間(レンダーとサーバー)で配列をやり取りしている。

サーバー側

  • index.js

レンダー側

  • render.js
  • subrender.js

全ソースは以下。
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Electron ipc test</title>
    <script type="text/javascript" src="render.js"></script>
  </head>
  <body>
    <div>ipc test !</div>
  </body>
</html>

index.js

'use strict';

var electron = require('electron');
var app = electron.app;
var BrowserWindow = electron.BrowserWindow;
var mainWindow = null;

app.on(
  'window-all-closed',
  function() {
    if (process.platform != 'darwin')
    app.quit();
  }
);
app.on(
  'ready',
  function() {
    mainWindow = new BrowserWindow({width: 500, height: 300});
    mainWindow.loadURL('file://' + __dirname + '/index.html');
    mainWindow.webContents.openDevTools();
    mainWindow.on(
      'closed',
      function() {
        mainWindow = null;
      }
    );
  }
);
electron.ipcMain.on(
  'hello-from-render',
  function(ev, arg) {
    console.log('receive data from render.');
    for(var key of Object.keys(arg))
      console.log('key=' + key + ' v=' + arg[key]);
    console.log('send data from server.');
    var ary = [];
    ary.push('A');
    ary.push('B');
    ary.push('C');
    ary['A'] = 'a';
    ary['B'] = 'b';
    ary['C'] = 'c';
    for(var key of Object.keys(ary))
      console.log('key=' + key + ' v=' + ary[key]);
    console.log('send ...');
    mainWindow.webContents.send('hello-from-server', ary);
  }
);

render.js

'use strict';

var electron = require('electron');

window.onload = function() {
  console.log('send data from render.');
  var ary = [];
  ary.push('A');
  ary.push('B');
  ary.push('C');
  ary['A'] = 'a';
  ary['B'] = 'b';
  ary['C'] = 'c';
  for(var key of Object.keys(ary))
    console.log('key=' + key + ' v=' + ary[key]);
  console.log('send ...');
  electron.ipcRenderer.send('hello-from-render', ary);
  console.log('send to sub ...');
  require('./subrender').emit('hello-from-render', ary);
}
electron.ipcRenderer.on(
  'hello-from-server',
  function(ev, arg) {
    console.log('receive data from server.');
    for(var key of Object.keys(arg))
      console.log('key=' + key + ' v=' + arg[key]);
  }
);

subrender.js

'use strict';

var EventEmitter = require('events').EventEmitter;
const event = new EventEmitter;
module.exports = event;

event.on(
  'hello-from-render',
  function(arg) {
    console.log('receive data from render by sub.');
    for(var key of Object.keys(arg))
      console.log('key=' + key + ' v=' + arg[key]);
  }
);

実行結果は以下の通り。

サーバー側のログ

receive data from render. <-- 連想配列の要素が欠落
key=0 v=A
key=1 v=B
key=2 v=C
send data from server.
key=0 v=A
key=1 v=B
key=2 v=C
key=A v=a
key=B v=b
key=C v=c
send ...

レンダー側のログ

send data from render.
key=0 v=A
key=1 v=B
key=2 v=C
key=A v=a
key=B v=b
key=C v=c
send ...
send to sub ...
receive data from render by sub. <-- EventEmitterでは連想配列もOK。
key=0 v=A
key=1 v=B
key=2 v=C
key=A v=a
key=B v=b
key=C v=c
receive data from server. <-- 連想配列の要素が欠落
key=0 v=A
key=1 v=B
key=2 v=C

添え字の配列は渡せるが、連想配列を渡すことはできなかった。
Jsonに変換できない、というのが理由でしょうか。
因みにNode.jsのイベントモジュールEventEmitterで送ればプロセス内なので連想配列も無事送ることができた。

three.jsで多色グラデーション

three.jsで多色グラデーションを作る。
多色グラデーション
9頂点8平面で構成されるメッシュにShaderMaterialで作成した多色グラデーションを貼り付けている。多色グラデーションは頂点シェーダーとフラグメントシェーダーの組み合わせで実現する。

頂点シェーダー

attribute vec3 verpos;
attribute vec3 verrgb;
varying vec3 varingrgb;
void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(verpos, 1.0);
  varingrgb = verrgb;
}

フラグメントシェーダー

varying mediump vec3 varingrgb;
void main() {
  gl_FragColor = vec4(varingrgb, 1.0);
}

verposには頂点の座標、verrgbには頂点の色が渡り、varingrgbでフラグメントシェーダーに引き渡される。頂点シェーダーの処理はVertexに対するが、フラグメントシェーダーはピクセルに対するため、GLSLが色を案分することでグラデーションが実現する。

3D操作はOrbitControls.jsを使っている。
OrbitControls.js

以下は全ソース(html)です。
直下にthree.min.jsとOrbitControls.jsがあれば動作します。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>shade test</title>
  <script type="text/javascript" src="three.min.js"></script>
  <script type="text/javascript" src="OrbitControls.js"></script>
  <script type="text/javascript">
    var renderer;
    var camera;
    var controls;
    var scene;
    var container;
    var light;
    var light2;
    var mesh;
    var geometry;
    function shadetest() {
      init_threejs();
      init_scene();
      init_camera();
      init_light();
      init_object();
      reset_position();
      loop();
    }
    function init_threejs() {
      renderer = new THREE.WebGLRenderer({antialias: true});
      renderer.setClearColor(0xffffff, 1);
      renderer.setSize(1, 1);
      container = document.getElementById('canvas3d');
      container.appendChild(renderer.domElement);
    }
    function init_scene() {
      scene = new THREE.Scene();
    }
    function init_camera() {
      camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100000);
      camera.up.x = 0;
      camera.up.y = 0;
      camera.up.z = 1;
      camera.position.set(0, 0, 5);
      controls = new THREE.OrbitControls(camera, renderer.domElement);
    }
    function init_light() {
      light = new THREE.DirectionalLight(0xcccccc, 1.6);
      light.position = new THREE.Vector3(-100, 500, 800);
      scene.add(light);
      light2 = new THREE.AmbientLight(0x333333);
      scene.add(light2);
    }
    function init_object() {
      var vertexPositions = [
        [-1.0, -1.0, 1.0],
        [0.0, -1.0, 1.0],
        [1.0, -1.0, 1.0],
        [-1.0, 0.0, 1.0],
        [0.0, 0.0, 1.0],
        [1.0, 0.0, 1.0],
        [-1.0, 1.0, 1.0],
        [0.0, 1.0, 1.0],
        [1.0, 1.0, 1.0]
      ];
      var vertexColors = [
        [1.0, 0.0, 0.0],
        [0.0, 1.0, 0.0],
        [0.0, 0.0, 1.0],
        [1.0, 1.0, 0.0],
        [0.0, 1.0, 1.0],
        [1.0, 0.0, 1.0],
        [0.5, 0.5, 0.0],
        [0.0, 0.5, 0.5],
        [0.5, 0.0, 0.5]
      ];
      var indices = new Uint16Array([
        0, 1, 4,
        1, 2, 4,
        2, 5, 4,
        5, 8, 4,
        8, 7, 4,
        7, 6, 4,
        6, 3, 4,
        3, 0, 4
      ]);
      var vertices = new Float32Array(vertexPositions.length * 3);
      var colors = new Float32Array(vertexPositions.length * 3);
      for (var i = 0; i < vertexPositions.length; i++) {
        vertices[i * 3 + 0] = vertexPositions[i][0];
        vertices[i * 3 + 1] = vertexPositions[i][1];
        vertices[i * 3 + 2] = vertexPositions[i][2];
        colors[i * 3 + 0] = vertexColors[i][0];
        colors[i * 3 + 1] = vertexColors[i][1];
        colors[i * 3 + 2] = vertexColors[i][2];
      }
      var shader = {
        fragmentShader: [
          "varying mediump vec3 varingrgb;",
          "void main() {",
            "gl_FragColor = vec4(varingrgb, 1.0);",
          "}"
        ].join("\n"),
        vertexShader: [
        
          "attribute vec3 verpos;",
          "attribute vec3 verrgb;",
          "varying vec3 varingrgb;",
          "void main() {",
            "gl_Position = projectionMatrix * modelViewMatrix * vec4(verpos, 1.0);",
            "varingrgb = verrgb;",
          "}"
        ].join("\n")
      }
      var material = new THREE.ShaderMaterial(
        {
          vertexShader: shader.vertexShader,
          fragmentShader: shader.fragmentShader
        }
      );
      var geometry = new THREE.BufferGeometry();
      geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
      geometry.setIndex(new THREE.BufferAttribute(indices,  1));
      geometry.addAttribute('verpos', new THREE.BufferAttribute(vertices, 3));
      geometry.addAttribute('verrgb', new THREE.BufferAttribute(colors, 3));
      var mesh = new THREE.Mesh(geometry, material);
      mesh.position.set(0, 0, 0);
      scene.add(mesh);
    }
    function reset_position() {
      var sz = {w:container.offsetWidth, h:container.offsetHeight};
      renderer.setSize(sz.w, sz.h);
      camera.aspect = sz.w / sz.h;
      camera.updateProjectionMatrix();
    }
    function loop() {
      requestAnimationFrame(loop);
      controls.update();
      renderer.clear();
      renderer.render(scene, camera);
    }
  </script>
</head>
<body onload="shadetest();" onresize="reset_position();">
  <div id="canvas3d" style="width:300px;height:300px;">
  </div>
</body>
</html>

IIS経由でDB認証エラー

ASP.netはVisual Studioでデバッグができるので、.netプログラマには相性が良い。

先刻、SQL serverにアクセスするWebアプリを作成したが、Visual Studioのデバッガーでは認証されるのに、IIS経由では認証エラーになってしまう。
エラーの詳細は以下。

「このログインで要求されたデータベース”○○○○○”を開けません。ログインに失敗しました。ユーザー’IIS APPPOOL\DefaultAppPool’はログインできませんでした。」
iis_login_error

どうやらDBにアクセスするワーカープロセスの権限がデバッガーとIISで異なることが原因のようだ。
「DefaultAppPool」というのは、IISのアプリケーションプールの名前だ。
名称からしてWindows認証のようだ。

それではMicrosoft SQL Server Management Studioでログインユーザーを作成してみよう。
ログイン名「IIS APPPOOL\DefaultAppPool」でWindows認証とする。

sqlserver_manstd_gen

ユーザーマッピングで目的のデータベースにdb_ownerのロールを与える。

sqlserver_manstd_umap

これで認証エラーは解消できるはずだ。
尚、ワーカープロセスのユーザー名はシステム環境により異なるようだ。
拙宅の場合「NT AUTHORITY\IUSR」というケースもあった。

InputMan for ASP.NETのバージョン移行

ASP.NET改修のお仕事でInputManを使う必要があり、同一バージョンの製品を購入し、さっそくVisualStudioで動作確認と思いきや、何故か例外が発生してしまう。

'WebDev.WebServer40.EXE' (マネージ (v4.0.30319)): 'C:\Windows\Microsoft.Net\assembly\GAC_MSIL\GrapeCity.Web.Input.v70\v4.0_7.0.2016.219__c3bd7c1dccef5128\GrapeCity.Web.Input.v70.dll' が読み込まれました
Application Error: System.Web.HttpParseException: 基本クラスに 'EditID' という名前のフィールドが含まれていますが、その型 (GrapeCity.Web.Input.IMEdit.GcTextBox) はコントロール (GrapeCity.Web.Input.IMEdit.GcTextBox) の型と互換性がありません。
場所 System.Web.Compilation.AssemblyBuilder.AddBuildProvider(BuildProvider buildProvider)
場所 System.Web.Compilation.BuildProvidersCompiler.ProcessBuildProviders()

どうやらDLLのバージョンとプログラムが認識しているバージョンが違うらしい。
インストールも正常にいってるし、プロジェクトにも最新のファイルが取り込まれているのに何故だ。

少々嵌っていると、なんとaspの宣言でバージョンが指定されているのを発見。

<%@ Register assembly="GrapeCity.Web.Input.v70, Version=7.0.2014.0122, ... %>

しかもビルドバージョン以降が違う。
購入version⇒7.0.2016.0219

これが原因なのか・・・。
該当箇所は多数だ。
相当な修正量である。

途方に暮れながらGrapeCityのサイトを眺めていると、バージョン移行ツールが付属しているとの事。

確かにあった。
inputman_tool

実行してみる。
inputman_ikou

7.0.2014.0122から7.0.2016.0219への変換と認識されている。
これを変換が必要なファイルに対して行う。
当方はcsprojに対して行ったが、他にconfig関係のファイルも指定できるようだ。

これで全て上手くいった。

FormBorderStyleの外観

.NetのFormはFormBorderStyleによりResizeの動作や枠の外観を変更できる。
確かにVisualStudioのデザイン画面上ではFormBorderStyleの変更で外観も追随する。
ところが実行してみるとWindows7上では枠の外観がデザイン画面のそれと一致しない。
少しおかしい…、バグなのか?
確かめてみよう。

以下のコード(DPI仮想化非対応)を用意した。

private void MainForm_Load(object sender, EventArgs e)
{
  labelMessage.Text = "FormBorderStyle = " +
                      this.FormBorderStyle.ToString();
}
private void buttonCopy_Click(object sender, EventArgs e)
{
  //Rectangle r = new Rectangle(Left, Top, Width, Height);
  Rectangle r = Bounds;
  Bitmap bmp = new Bitmap(r.Width, r.Height,
                          PixelFormat.Format32bppArgb);
  Graphics g = Graphics.FromImage(bmp);
  Point sOrg = new Point(r.Left, r.Top);
  Point dOrg = new Point(0, 0);
  g.CopyFromScreen(sOrg, dOrg, 
                   new Size(r.Width, r.Height),
                   CopyPixelOperation.SourceCopy);
  Clipboard.SetImage(bmp);
  bmp.Dispose();
  MessageBox.Show("画面キャプチャをClipboardに転送しました。", 
                  "確認",
                  MessageBoxButtons.OK, MessageBoxIcon.Information);
}

ボタンを押すと自身のFormをキャプチャする仕組みだ。
キャプチャの範囲はFormのBoundsを使う。
BoundsとはFormのLeft、Top、Width、Heightを保持するプロパティだ。
これできっちりキャプチャが取れるはずだ。
以下は全てのFormBorderStyleでキャプチャした結果である。
画面上にFormBorderStyleを表示している。

FormBorderStyle=None
fbs-none

FormBorderStyle=SizableToolWindow
fbs-sizabletoolwindow

FormBorderStyle=FixedToolWindow
fbs-fixedtoolwindow

FormBorderStyle=Sizable
fbs-sizable

FormBorderStyle=FixedDialog
fbs-fixeddialog

FormBorderStyle=Fixed3D
fbs-fixed3d

FormBorderStyle=FixedSingle
fbs-fixedsingle

None、SizableToolWindow、Sizable以外は微妙に端が切れてキャプチャされている。
FormのBoundsと異なる形状で表示されていることがわかるだろう。

やはりこれはバグなのか?
.Net APIが返却するFormのBoundsと外観の違いは、VisualStudioのデザイン画面との食い違いを見ても明らかである。
とは言え、この現象はWindows10に於いても改善されていない。
何らかの意味のある「仕様」なのか?

わからないのである…。

起動時にMainFormを非表示にする

.netのMain form、即ちApplication.Runに渡すアプリケーションのメインウィンドウとなるFormオブジェクトだが、これを起動時に非表示にする真っ当な方法が無い。
WinAPI時代はPreCreateWindow等の気の利いたタイミングでWS_VISIBLEをオフする手があったが、どうも.netにはこのような手段が無い…、ようだ。
ググってみても、Application.Runに渡さずShowDialogしろとか、ActivatedハンドラでHideしろとか、なんだかすっきりしない。

そこで非表示は諦めて、透明度(Opacity)で代替してみた。
以下は初期表示にOpacityを0.0に設定(非表示状態)し、徐々に透明度を下げる(1.0に近づける)ことで1.5秒後にFormの表示が完了するようにしている。

private void Form_Load(object sender, EventArgs e)
{
  Timer timer = new Timer();
  timer.Tick += new EventHandler(OpeningTimerProc);
  timer.Interval = 10;
  timer.Start();
  timer.Tag = System.DateTime.Now.Ticks;
  this.Opacity = 0.0;
}
private void OpeningTimerProc(object sender, EventArgs e)
{
  if (sender is Timer)
  {
    long DISPLAY_TIME = 15000000;
    Timer timer = (Timer)sender;
    long w = System.DateTime.Now.Ticks - (long)timer.Tag;
    if (w >= DISPLAY_TIME)
    {
      this.Opacity = 1.0;
      timer.Stop();
    }
    else
    {
      this.Opacity = 1.0 * w / DISPLAY_TIME;
    }
  }
}

上記はアプリケーションのメインウィンドウとなるFormクラスで、Form_LoadはLoadハンドラだ。
開始時間をTagに格納し、タイマーで透明度を下げる時間の判定に使っている。
Tagはなんだか便利だ。
もちろんいきなりOpacityを1.0にしても良いが、1.5秒もアプリの起動操作に反応が無い、というのも宜しくないので、このようにしてみた。

ToolTipのバルーンをコントロール以外の図形に表示する

.NetのバルーンツールはToolTipだが、コントロールではないオブジェクト、例えば自作の図形に適用するには、少々細工が必要だ。
バルーンを表示するタイミングはMouseMoveイベントで自作図形の範囲にマウスが侵入する時だろう。

private void MouseMove(object sender, System.Windows.Forms.MouseEventArgs e) {
  System.Drawing.Point pt = new System.Drawing.Point(e.X, e.Y);
  Rectangle rect = myShape.GetExtent();
  if(rect.Contains(pt)) {
    toolTip.Show("こんにちは、図形!", pt, 3000); // 3秒間バルーン表示
  }
}

myShapeは自作図形、GetExtentは図形の存在領域を物理単位で返す関数、toolTipは.netのToolTipコントロールだ。
これを実行してみる。
自作図形の上にマウスを置くと、バルーン「こんにちは、図形!」が表示されるが、チラつきが激しい。
美しくない。
MouseMove発生毎にtoolTip.Showが呼ばれるのがチラつきの原因だろう。
ToolTipコントロールがバルーン表示中に呼ばれるShowを無視してくれることを期待したが、甘かった。
ここは表示制御が必要のようだ。

バルーン表示直後に表示中フラグを設け、Timerイベントのハンドラでこれをクリアする。
このロジックでバルーン表示中のShow呼び出しを抑制してみよう。

bool balloonDisplay = false;
private void MouseMove(object sender, System.Windows.Forms.MouseEventArgs e) {
  System.Drawing.Point pt = new System.Drawing.Point(e.X, e.Y);
  Rectangle rect = myShape.GetExtent();
  if(!balloonDisplay && rect.Contains(pt)) {
    toolTip.Show("こんにちは、図形!", pt, 3000); // 3秒間バルーン表示
    balloonDisplay = true;
    Timer timer = new Timer();
    timer.Tick += new EventHandler(ClearEvent);
    timer.Interval = 5000; // 5秒後にバルーン表示抑制解除
    timer.Start();
  }
}
private void ClearEvent(object sender, EventArgs e) {
  ((Timer)sender).Stop();
  balloonDisplay = false;
}

バルーン表示時間と抑制解除の時間は適宜変更するとよい。