using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Windows.Media.Media3D;
using System.Xml.Serialization;
using TIS.Imaging;
namespace TIASshot {
public partial class Form1 : Form {
private CameraBase _camera;
public static string APP_NAME = "TIAS Shot";
private LightSource _light = new LightSource();
private bool _isLightOn = true;
private PreviewMonitor _previewMonitor;
private Bitmap _dispBuf = null;
private bool _isUpdateBuf = false;
private readonly object _dispBufLock = new object();
// 本モニタ(picDisplay)の自ペース再描画用タイマー。
// 以前はコールバックスレッドから毎フレーム Invalidate() していたが,Form1 の WM_PAINT が
// 常時保留状態になり,同一 UI スレッドを共有する副モニタの WM_TIMER が
// (WM_PAINT > WM_TIMER の優先度により)飢餓状態になっていた。
// 本モニタも副モニタと同じタイマー駆動に統一し,コールバックスレッドからは
// Invalidate を一切呼ばないことで WM_PAINT の常時保留(洪水源)を消す。
private const int MainRefreshIntervalMs = 33; // ≒30fps(PreviewMonitor と同じ値)
private readonly System.Windows.Forms.Timer _mainRefreshTimer = new System.Windows.Forms.Timer();
// アプリを閉じ始めたか(Form1_FormClosing で閉じると決めた時点で立てる).
// 立った後は,カメラのコールバック・保存スレッド由来の UI 呼び出し(ShowMessage / EnableShots)を何もしない.
// 切断(SDK のストリーム停止)がコールバックの終了を待つ間に,コールバック側が UI スレッドを待つと
// 相互待機でハングするため.別スレッドから読むため volatile とする.
private volatile bool _isClosing = false;
/// <summary>保存中に閉じようとしたときの表示</summary>
internal const string SavingOnCloseMessage = "保存中です.保存完了後に閉じてください";
/// <summary>
/// コンストラクタ
/// </summary>
public Form1() {
InitializeComponent();
#if WHITEBOARD
InitializeWhiteBoardButton();
#endif
#if MEMMONITOR
this.Shown += (s, e) => ShowMemoryMonitor();
this.KeyPreview = true;
this.KeyDown += Form1_MemMonitor_KeyDown;
#endif
var version = Assembly.GetExecutingAssembly().GetName().Version;
Text = $"{APP_NAME} ver.{version.Major}.{version.Minor}";
#if DEBUG
Text += " [DEBUG]";
#endif
}
/// <summary>
/// フォームロード
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Form1_Load(object sender, EventArgs e) {
Config.Load();
_camera = new Lucam(this, picPreview);
if (!_camera.Connect()) {
Debug.WriteLine(_camera.ErrorMsg);
Debug.WriteLine("Lucamの接続に失敗しました");
_camera = null;
}
if (_camera == null) {
var cameraList = icImagingControl1.Devices.Select(d => d.Name.Replace(" ", ""));
foreach (var cam in cameraList) {
_camera = new IScam(this, icImagingControl1, cam);
if (_camera.Connect()) break;
Debug.WriteLine(_camera.ErrorMsg);
Debug.WriteLine($"{cam}の接続に失敗しました");
_camera = null;
}
}
if (_camera == null) {
MessageBox.Show("カメラが見つかりません.終了します.", APP_NAME,
MessageBoxButtons.OK, MessageBoxIcon.Warning);
Close();
return;
}
txtDeviceName.Text = _camera.DeviceName;
txtSerialNo.Text = _camera.SerialNumber;
txtNumMultiShots.Text = Config.GetString("Shot/MultiShotCount");
txtMultiShotsInterval.Text = Config.GetString("Shot/MultiShotInterval");
txtDataName.Text = Config.GetString("Shot/SubjectName");
txtSaveFolder.Text = Config.GetString("File/SaveFolder");
txtConvElemNum.Text = Config.GetString("Calib/ConversionChannels");
EnableShots(false);
//_lucam.StartStopPreview();
if (_light.Open()) {
_light.SetLight();
} else {
Debug.WriteLine("光源の接続に失敗しました");
btnLightSW.Enabled = false;
}
_previewMonitor = new PreviewMonitor();
if (_previewMonitor.IsShown) _previewMonitor.Show();
_mainRefreshTimer.Interval = MainRefreshIntervalMs;
_mainRefreshTimer.Tick += MainRefreshTimer_Tick;
_mainRefreshTimer.Start();
}
/// <summary>
/// フォームクローズ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Form1_FormClosing(object sender, FormClosingEventArgs e) {
if (!CanClose(_camera != null && _camera.IsSaving)) {
// 保存中に閉じると未保存の画像が失われるため,閉じるのを取り消して知らせる
e.Cancel = true;
ShowMessage(SavingOnCloseMessage);
return;
}
ShutdownOnClose();
}
/// <summary>
/// アプリを閉じてよいか(保存中は閉じない).
/// </summary>
/// <param name="isSaving">画像保存スレッドが動作中か(CameraBase.IsSaving)</param>
/// <returns>閉じてよければ true</returns>
internal static bool CanClose(bool isSaving) {
return !isSaving;
}
/// <summary>
/// 閉じるときの後始末(Form1_FormClosing で閉じると決めた後に呼ぶ).
/// 【重要】順序は「閉じ始めのフラグ → タイマー停止 → カメラ切断 → 光源を消してポートを閉じる」を守ること.
/// フラグを先に立てることで,切断がコールバックの終了を待つ間にコールバック側が UI 呼び出しで
/// UI スレッドを待つことがなくなる(ハングの防止).カメラの切断が例外になっても光源は閉じる.
/// </summary>
private void ShutdownOnClose() {
_isClosing = true;
OnCloseStep("closing");
_mainRefreshTimer.Stop();
OnCloseStep("timer");
try {
_camera?.Disconnect();
OnCloseStep("camera");
} finally {
_light.Close();
OnCloseStep("light");
}
}
/// <summary>
/// 閉じるときの後始末の各段階を通過したことの記録(SELFTEST でのみ実装.Release では呼び出しごと消える).
/// </summary>
/// <param name="step">段階名(closing / timer / camera / light)</param>
partial void OnCloseStep(string step);
/// <summary>
/// 画像撮影1枚
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnShot1_Click(object sender, EventArgs e) {
ExecuteShot(() => _camera.ShotOne());
}
/// <summary>
/// 複数画像撮影
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnShotMulti_Click(object sender, EventArgs e) {
ExecuteShot(() => _camera.ShotMulti());
}
/// <summary>
/// 撮影の例外境界.撮影ボタンを無効化して撮影処理を実行し,
/// 想定外の例外が発生した場合はユーザーに通知する.
/// 例外を捕捉しないと WinForms の未処理例外ダイアログになるうえ,
/// SaveThread を起動する前に失敗した場合は EnableShots(true) に誰も到達せず,
/// 撮影ボタンが無効のまま固着してアプリの再起動が必要になる.
/// 【重要】SaveThread 起動後(IsSaveThreadStarted == true)は再有効化してはならない.
/// 撮影が例外で終わっても保存はまだ続いており,ここで先に有効化すると保存中に次の撮影へ
/// 再入できてしまい,UI スレッドの _saveThread.Join() と SaveThread 側の Control.Invoke が
/// 相互待機してアプリがハングする.この経路の再有効化は SaveThread の finally が必ず行う
/// (CompleteAdding には全経路で到達するため SaveThread は必ず完走する).
/// 撮影の中止通知(CaptureFailedException 由来)は例外ではなく CameraBase 側が
/// 撮影終了処理の完了後に行うため,ここでは扱わない.
/// </summary>
/// <param name="shot">撮影処理(ShotOne / ShotMulti)</param>
private void ExecuteShot(Action shot) {
EnableShots(false);
try {
shot();
} catch (Exception ex) {
Debug.WriteLine($"撮影に失敗しました: {ex}");
// カメラの状態復帰(EndShot)と SaveThread の終了通知は Shot 内の finally で完了済み.
// SaveThread 未起動の経路だけをここで復帰させる(起動済みなら SaveThread に任せる).
if (_camera == null || !_camera.IsSaveThreadStarted) {
EnableShots(true);
}
ShowError($"撮影に失敗しました.\r\n{ex.Message}");
}
}
/// <summary>
/// 画像表示
/// ワーカースレッドから呼び出される
/// </summary>
/// <param name="bmp"></param>
public void ShowImage(Bitmap bmp) {
#if MEMMONITOR
PreviewProfiler.IncrementCallbackFrames();
PreviewProfiler.SetLastImageSize(bmp.Width, bmp.Height);
long mainCopyStart = System.Diagnostics.Stopwatch.GetTimestamp();
#endif
// Lucam 経路は二重バッファ(_bmps[0]/_bmps[1])をコールバックスレッド上で Dispose するため,
// 入力 bmp をそのまま picDisplay.Image に載せると,UIスレッドの OnPaint が Width 等を
// 読んでいる最中にコールバックスレッドが Dispose して別スレッド同時アクセスでクラッシュしうる。
// そのため ShowImage は入力 bmp を「読むだけ」とし,表示用には呼び出しスレッド上で
// Form1 所有のコピーを生成する(bmp の所有権は呼び出し側に残す)。
// BitmapUtil.FastClone は LockBits による生画素コピーで,GDI+ の DrawImage 経路を使う
// new Bitmap(bmp)(~25ms)より高速(数 ms)。生成されるコピーは入力と独立した画素バッファを
// 持つため,所有権・スレッド安全性のモデルは new Bitmap(bmp) と完全に同一。
var dispCopy = BitmapUtil.FastClone(bmp);
#if MEMMONITOR
PreviewProfiler.AddMainCopyTicks(System.Diagnostics.Stopwatch.GetTimestamp() - mainCopyStart);
#endif
// 原因2: タイマー Tick に反映されなかった取りこぼしフレームを Dispose する
// _dispBufLock で ShowImage(コールバックスレッド)と MainRefreshTimer_Tick(UIスレッド)の
// 同時アクセスを防ぎ、二重 Dispose・使用中バッファの誤 Dispose を回避する
Bitmap toDispose = null;
lock (_dispBufLock) {
if (_isUpdateBuf && _dispBuf != null) {
toDispose = _dispBuf; // タイマー未反映の旧バッファを破棄予約
}
_dispBuf = dispCopy;
_isUpdateBuf = true;
}
toDispose?.Dispose(); // ロック外で GDI 解放(ロック保持時間を最小化)
// Invalidate() は呼ばない。毎フレーム呼ぶと Form1 の WM_PAINT が常時保留状態になり,
// 同一 UI スレッドを共有する副モニタの WM_TIMER(WM_PAINT より優先度が低い)が
// 飢餓状態になるため(本モニタも MainRefreshTimer_Tick による自ペース再描画に統一した)。
// 原因1: 副モニタが表示されているときだけフルコピーを生成する
// (非表示時に毎フレーム bmp2 を生成しリークしていた問題を修正)
bool feedSub = _previewMonitor != null && _previewMonitor.IsShown;
#if MEMMONITOR
// F9 トグル(ToggleSubMonitorProfiling)による計測用の一時抑制。
// 抑制中は副モニタ用コピー生成も UpdateImage 呼び出しも走らせず,
// コールバックスレッドのコピー回数を本モニタ用の 1 回だけに戻す。
if (_subMonitorSuppressed) feedSub = false;
#endif
if (feedSub) {
#if MEMMONITOR
PreviewProfiler.IsSubShown = true;
long subCopyStart = System.Diagnostics.Stopwatch.GetTimestamp();
var subCopy = BitmapUtil.FastClone(bmp);
PreviewProfiler.AddSubCopyTicks(System.Diagnostics.Stopwatch.GetTimestamp() - subCopyStart);
_previewMonitor.UpdateImage(subCopy);
#else
_previewMonitor.UpdateImage(BitmapUtil.FastClone(bmp));
#endif
}
#if MEMMONITOR
else {
PreviewProfiler.IsSubShown = false;
}
#endif
}
/// <summary>
/// 本モニタ(picDisplay)の自ペース再描画(約30fps)。
/// 新フレームがあるときだけ picDisplay.Image を差し替える(静止フレームは無駄に再描画しない)。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MainRefreshTimer_Tick(object sender, EventArgs e) {
// 原因2: _dispBufLock で ShowImage との競合を防ぎながらバッファを取り出す
// _dispBuf を null に戻すことで ShowImage 側が「表示中バッファ」を誤って Dispose しないようにする
Bitmap toDisplay;
lock (_dispBufLock) {
if (!_isUpdateBuf || _dispBuf == null) return;
toDisplay = _dispBuf;
_dispBuf = null; // 参照を手放し、次の ShowImage で二重 Dispose されないようにする
_isUpdateBuf = false;
}
// picDisplay.Image の Dispose・差し替えは UI スレッド(このタイマー Tick)でのみ行う。
picDisplay.Image?.Dispose(); // 直前に表示していたバッファを解放
picDisplay.Image = toDisplay;
#if MEMMONITOR
PreviewProfiler.IncrementMainPaints();
#endif
}
/// <summary>
/// メッセージ表示
/// </summary>
/// <param name="msg"></param>
public void ShowMessage(string msg) {
// UI スレッド外(カメラのコールバック・保存スレッド)からの呼び出しは UI スレッドの処理を待たずに投稿する
// (BeginInvoke.撮影中で UI スレッドが塞がっていても保存が進み,画像がキューに溜まり続けないため).
// 閉じ始めた後・ハンドルが無い/破棄済みのときは何もしない.
if (!CanUpdateUi()) return;
if (InvokeRequired) {
PostToUi(delegate { ShowMessage(msg); });
return;
}
#if SELFTEST
SelfTestRecordMessage(msg);
#endif
txtMessage.Text = msg;
txtMessage.Update();
}
/// <summary>
/// エラー通知(モーダルダイアログ)
/// 撮影中止など,ステータス表示(ShowMessage)では見落とされては困る事象をユーザーに明示通知する.
/// 撮影スレッド・保存スレッドから呼ばれるため UI スレッドへマーシャリングする.
/// </summary>
/// <param name="msg">表示するメッセージ</param>
public void ShowError(string msg) {
if (InvokeRequired) {
Invoke((MethodInvoker)delegate { ShowError(msg); });
return;
}
#if SELFTEST
// ヘッドレスのセルフテストではモーダル表示すると無人実行が止まるため,記録のみ行う
if (_selfTestHeadless) {
SelfTestRecordError(msg);
return;
}
#endif
MessageBox.Show(msg, APP_NAME, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
/// <summary>
/// データ名取得
/// </summary>
/// <returns></returns>
public string GetDataName() {
if (InvokeRequired) {
return (string)Invoke(new Func<string>(GetDataName));
}
return txtDataName.Text;
}
/// <summary>
/// マルチショット撮影の枚数取得
/// </summary>
/// <returns></returns>
public int GetNumMultiShots() {
if (InvokeRequired) {
return (int)Invoke(new Func<int>(GetNumMultiShots));
}
if (int.TryParse(txtNumMultiShots.Text, out int result)) return result;
return 1;
}
/// <summary>
/// マルチショット撮影の間隔取得
/// </summary>
/// <returns></returns>
public int GetMultiShotsInterval() {
if (InvokeRequired) {
return (int)Invoke(new Func<int>(GetMultiShotsInterval));
}
if (int.TryParse(txtMultiShotsInterval.Text, out int result)) return result;
return 1000;
}
/// <summary>
/// 撮影ボタン有効化
/// </summary>
/// <param name="enable"></param>
public void EnableShots(bool enable = true) {
// UI スレッド外からの呼び出しは ShowMessage と同じく待たずに投稿する(同じスレッドから投稿した
// ShowMessage・ShowError との順序は保たれる).閉じ始めた後・ハンドルが無い/破棄済みのときは何もしない.
if (!CanUpdateUi()) return;
if (InvokeRequired) {
PostToUi(delegate { EnableShots(enable); });
return;
}
btnShotOne.Enabled = enable;
btnShotMulti.Enabled = enable;
}
/// <summary>
/// 通知系の UI 呼び出し(ShowMessage / EnableShots)を行ってよいか.
/// 閉じ始めた後(_isClosing)・破棄中/破棄済み・ハンドル未生成のときは false.
/// </summary>
private bool CanUpdateUi() {
return !_isClosing && !IsDisposed && !Disposing && IsHandleCreated;
}
/// <summary>
/// UI スレッドへ処理を投稿する(完了を待たない).
/// 判定(CanUpdateUi)の直後にハンドルが破棄されると BeginInvoke が例外を投げるため,
/// 捕捉してログのみ残す(コールバック・保存スレッドへ例外を伝えない).
/// </summary>
/// <param name="action">UI スレッドで実行する処理</param>
private void PostToUi(MethodInvoker action) {
try {
BeginInvoke(action);
} catch (InvalidOperationException ex) {
// ObjectDisposedException(InvalidOperationException の派生)を含む
Debug.WriteLine($"UI スレッドへの投稿に失敗しました(フォームの終了中): {ex.Message}");
}
}
/// <summary>
/// データフォルダを開く
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnOpenDataFolder_Click(object sender, EventArgs e) {
System.Diagnostics.Process.Start(txtSaveFolder.Text);
}
/// <summary>
/// 光源のON/OFF切り替え
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnLightSW_Click(object sender, EventArgs e) {
if (!_light.IsOpen || _camera.IsCalibrating) return;
if (_isLightOn) {
_light.TurnOff();
btnLightSW.Text = "照明 ON";
_isLightOn = false;
} else {
_light.SetLight();
btnLightSW.Text = "照明 OFF";
_isLightOn = true;
}
}
}
}