Newer
Older
TIASShot / TIASshot / UI / Form1.cs
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;
            }
        }
    }
}