Newer
Older
LensAdjustmentTool / MainForm.cs
using LensAdjustmentTool.Camera;
using LensAdjustmentTool.Configuration;
using LensAdjustmentTool.Infrastructure;
using LensAdjustmentTool.Models;
using LensAdjustmentTool.Processing;
using System;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Windows.Forms;

namespace LensAdjustmentTool
{
    public partial class MainForm : Form
    {
        private readonly AppLogger _logger;
        private readonly LatestResultMailbox _mailbox;
        private readonly LensAdjustmentProcessor _processor;
        private readonly IcImagingCameraService _camera;
        private readonly ApertureRoiSettingsStore _apertureRoiSettingsStore;
        private readonly Timer _uiTimer;

        private TabControl _tabs;
        private TabPage _apertureTabPage;
        private TabPage _focusTabPage;
        private PictureBox _focusPicture;
        private Label _focusCurrentValue;
        private Label _focusMaxValue;
        private Button _focusResetButton;

        private SplitContainer _focusSplit;
        private SplitContainer _apertureSplit;
        private PictureBox _aperturePicture;
        private Label _apertureCurrentValue;
        private Label _apertureDifferenceValue;
        private Label _apertureInstructionLabel;

        private ToolStripStatusLabel _cameraStatusLabel;
        private ToolStripStatusLabel _cameraInfoLabel;
        private ToolStripStatusLabel _fpsLabel;

        private int _displayedFrameCount;
        private DateTime _fpsWindowStart = DateTime.Now;
        private bool _closing;

        public MainForm()
        {
            InitializeComponent();
            BuildUserInterface();

            _logger = new AppLogger();
            _mailbox = new LatestResultMailbox();
            _processor = new LensAdjustmentProcessor();
            _apertureRoiSettingsStore = new ApertureRoiSettingsStore();
            _camera = new IcImagingCameraService(icImagingControl1, _processor, _mailbox, _logger);

            _uiTimer = new Timer { Interval = AppSettings.UiRefreshIntervalMs };
            _uiTimer.Tick += UiTimer_Tick;

            Shown += MainForm_Shown;
            FormClosing += MainForm_FormClosing;
            _tabs.SelectedIndexChanged += Tabs_SelectedIndexChanged;
            _focusResetButton.Click += FocusResetButton_Click;

            var version = Assembly.GetExecutingAssembly().GetName().Version;
            Text = $"DFK33UX178 Lens Adjustment Tool ver.{version.Major}.{version.Minor}";
#if DEBUG
            Text += " [DEBUG]";
#endif
        }

        private void BuildUserInterface()
        {
            WindowState = FormWindowState.Maximized;
            MinimumSize = new Size(1100, 700);

            _tabs = new TabControl
            {
                Dock = DockStyle.Fill,
                Font = new Font("Yu Gothic UI", 16F),
                ItemSize = new Size(220, 48),
                SizeMode = TabSizeMode.Fixed,
                DrawMode = TabDrawMode.OwnerDrawFixed
            };
            _tabs.DrawItem += Tabs_DrawItem;

            _focusTabPage = new TabPage("フォーカス調整");
            _apertureTabPage = new TabPage("開口調整");

            BuildFocusPage(_focusTabPage);
            BuildAperturePage(_apertureTabPage);

            _tabs.TabPages.Add(_apertureTabPage);
            _tabs.TabPages.Add(_focusTabPage);
            _tabs.SelectedIndex = 0;

            var statusStrip = new StatusStrip();
            _cameraStatusLabel = new ToolStripStatusLabel("Camera: 未初期化");
            _cameraInfoLabel = new ToolStripStatusLabel { Spring = true, TextAlign = ContentAlignment.MiddleLeft };
            _fpsLabel = new ToolStripStatusLabel("UI: 0.0 fps");
            statusStrip.Items.AddRange(new ToolStripItem[]
            {
                _cameraStatusLabel,
                new ToolStripStatusLabel(" | "),
                _cameraInfoLabel,
                _fpsLabel
            });

            Controls.Add(_tabs);
            Controls.Add(statusStrip);
            _tabs.BringToFront();
            statusStrip.BringToFront();
        }

        private void BuildFocusPage(TabPage page)
        {
            _focusSplit = new SplitContainer
            {
                Dock = DockStyle.Fill,
                FixedPanel = FixedPanel.Panel2
            };
            var split = _focusSplit;

            _focusPicture = CreatePictureBox();
            split.Panel1.Controls.Add(_focusPicture);

            var panel = CreateSidePanel();
            split.Panel2.Controls.Add(panel);

            AddHeader(panel, "フォーカス評価値");
            AddCaption(panel, "現在値");
            _focusCurrentValue = AddValueLabel(panel, "--");
            AddCaption(panel, "最大値");
            _focusMaxValue = AddValueLabel(panel, "--");

            _focusResetButton = new Button
            {
                Text = "最大値をリセット",
                Width = 310,
                Height = 48,
                Font = new Font("Yu Gothic UI", 12F),
                Margin = new Padding(3, 10, 3, 10)
            };
            panel.Controls.Add(_focusResetButton);

            AddInstruction(
                panel,
                "最初にフォーカスを広くゆっくり回してください。\r\n\r\n" +
                "その後、現在値が最大値に近くなるように\r\nレンズのフォーカスを調整してください。\r\n\r\n" +
                "ROI: (2135, 800), 400×600\r\n表示: 右90°回転 → 600×400");
            AddInstructionImage(panel, "LensFocus.jpg");

            page.Controls.Add(split);
            split.Panel2MinSize = 340;
            _focusSplit.Layout += SplitContainer_Layout;
        }

        private void BuildAperturePage(TabPage page)
        {
            _apertureSplit = new SplitContainer
            {
                Dock = DockStyle.Fill,
                FixedPanel = FixedPanel.Panel2
            };
            var split = _apertureSplit;

            _aperturePicture = CreatePictureBox();
            split.Panel1.Controls.Add(_aperturePicture);

            var panel = CreateSidePanel();
            split.Panel2.Controls.Add(panel);

            AddHeader(panel, "白色票 平均画素値");
            AddCaption(panel, "現在値");
            _apertureCurrentValue = AddValueLabel(panel, "--");
            AddCaption(panel, "目標値");
            AddValueLabel(panel, AppSettings.ApertureTarget.ToString("0.0"));
            AddCaption(panel, "差(現在値 − 目標値)");
            _apertureDifferenceValue = AddValueLabel(panel, "--");

            //AddCaption(panel, "ROI位置微調整");
            _apertureInstructionLabel = AddInstruction(
                panel,
                BuildApertureInstructionText(AppSettings.ApertureCalculationRoi));

            panel.Controls.Add(CreateApertureRoiMovePanel());

            AddInstructionImage(panel, "LensApature.jpg");

            page.Controls.Add(split);
            split.Panel2MinSize = 340;
            _apertureSplit.Layout += SplitContainer_Layout;
        }

        private void SetSplitterDistance(SplitContainer split)
        {
            int available = split.Width;
            if (available <= split.Panel1MinSize + split.Panel2MinSize + split.SplitterWidth)
                return;
            int distance = Math.Max(
                available - split.Panel2MinSize - split.SplitterWidth,
                split.Panel1MinSize);
            split.SplitterDistance = distance;
        }

        private static PictureBox CreatePictureBox()
        {
            return new PictureBox
            {
                Dock = DockStyle.Fill,
                BackColor = Color.Black,
                SizeMode = PictureBoxSizeMode.Zoom,
                BorderStyle = BorderStyle.FixedSingle
            };
        }

        private static FlowLayoutPanel CreateSidePanel()
        {
            return new FlowLayoutPanel
            {
                Dock = DockStyle.Fill,
                FlowDirection = FlowDirection.TopDown,
                WrapContents = false,
                AutoScroll = true,
                Padding = new Padding(18),
                BackColor = SystemColors.Control
            };
        }

        private static void AddHeader(Control parent, string text)
        {
            parent.Controls.Add(new Label
            {
                Text = text,
                AutoSize = false,
                Width = 310,
                Height = 48,
                Font = new Font("Yu Gothic UI", 20F, FontStyle.Bold),
                TextAlign = ContentAlignment.MiddleLeft
            });
        }

        private TableLayoutPanel CreateApertureRoiMovePanel()
        {
            var panel = new TableLayoutPanel
            {
                ColumnCount = 3,
                RowCount = 3,
                Width = 150,
                Height = 110,
                Margin = new Padding(3, 4, 3, 12)
            };

            panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 46F));
            panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 46F));
            panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 46F));
            panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 32F));
            panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 32F));
            panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 32F));

            panel.Controls.Add(CreateApertureRoiMoveButton("↑", 0, -AppSettings.ApertureRoiMoveStep), 1, 0);
            panel.Controls.Add(CreateApertureRoiMoveButton("←", -AppSettings.ApertureRoiMoveStep, 0), 0, 1);
            panel.Controls.Add(CreateApertureRoiMoveButton("→", AppSettings.ApertureRoiMoveStep, 0), 2, 1);
            panel.Controls.Add(CreateApertureRoiMoveButton("↓", 0, AppSettings.ApertureRoiMoveStep), 1, 2);

            return panel;
        }

        private Button CreateApertureRoiMoveButton(string text, int deltaX, int deltaY)
        {
            var button = new Button
            {
                Text = text,
                Width = 40,
                Height = 28,
                Font = new Font("Yu Gothic UI", 10F),
                Margin = new Padding(3),
                Tag = new Point(deltaX, deltaY)
            };
            button.Click += ApertureRoiMoveButton_Click;
            return button;
        }

        private static void AddCaption(Control parent, string text)
        {
            parent.Controls.Add(new Label
            {
                Text = text,
                AutoSize = false,
                Width = 310,
                Height = 32,
                Font = new Font("Yu Gothic UI", 11F),
                TextAlign = ContentAlignment.BottomLeft
            });
        }

        private static Label AddValueLabel(Control parent, string text)
        {
            var label = new Label
            {
                Text = text,
                AutoSize = false,
                Width = 310,
                Height = 76,
                Font = new Font("Consolas", 32F, FontStyle.Bold),
                TextAlign = ContentAlignment.MiddleRight,
                BorderStyle = BorderStyle.FixedSingle,
                BackColor = Color.White
            };
            parent.Controls.Add(label);
            return label;
        }

        private static Label AddInstruction(Control parent, string text)
        {
            var label = new Label
            {
                Text = text,
                AutoSize = false,
                Width = 310,
                Height = 230,
                Font = new Font("Yu Gothic UI", 12F),
                TextAlign = ContentAlignment.MiddleLeft,
                Padding = new Padding(8)
            };
            parent.Controls.Add(label);
            return label;
        }

        private static string BuildApertureInstructionText(Rectangle calculationRoi)
        {
            return "現在値が目標値(±1.0)となるように\r\nレンズの開口を調整してください。\r\n\r\n" +
                "ROI位置微調整\r\n" +
                $"赤枠: ROI ({calculationRoi.X}, {calculationRoi.Y}), {calculationRoi.Width}×{calculationRoi.Height}\r\n" +
                //$"表示ROI: ({AppSettings.ApertureDisplayRoi.X}, {AppSettings.ApertureDisplayRoi.Y}), {AppSettings.ApertureDisplayRoi.Width}×{AppSettings.ApertureDisplayRoi.Height}\r\n" +
                $"ROI移動: {AppSettings.ApertureRoiMoveStep}px単位";
        }

        private void UpdateApertureInstructionText(Rectangle calculationRoi)
        {
            if (_apertureInstructionLabel != null)
            {
                _apertureInstructionLabel.Text = BuildApertureInstructionText(calculationRoi);
            }
        }

        private static void AddInstructionImage(Control parent, string fileName)
        {
            string path = FindMediaFilePath(fileName);
            if (path == null)
                return;

            Image image;
            using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (var source = Image.FromStream(stream))
            {
                image = new Bitmap(source);
            }

            var pictureBox = new PictureBox
            {
                Width = 310,
                Height = 464,
                SizeMode = PictureBoxSizeMode.Zoom,
                BorderStyle = BorderStyle.FixedSingle,
                BackColor = Color.White,
                Margin = new Padding(3, 8, 3, 8),
                Image = image
            };
            pictureBox.Disposed += (sender, e) => pictureBox.Image?.Dispose();
            parent.Controls.Add(pictureBox);
        }

        private static string FindMediaFilePath(string fileName)
        {
            string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
            string[] candidates =
            {
                Path.Combine(baseDirectory, "Media", fileName),
                //Path.Combine(baseDirectory, fileName),
                //Path.GetFullPath(Path.Combine(baseDirectory, @"..\Media", fileName)),
                //Path.GetFullPath(Path.Combine(baseDirectory, @"..\..\Media", fileName)),
                //Path.GetFullPath(Path.Combine(baseDirectory, @"..\..\..\Media", fileName))
            };

            foreach (string candidate in candidates)
            {
                if (File.Exists(candidate))
                    return candidate;
            }

            return null;
        }

        private void Tabs_DrawItem(object sender, DrawItemEventArgs e)
        {
            TabPage page = _tabs.TabPages[e.Index];
            bool selected = e.Index == _tabs.SelectedIndex;

            Color backColor = selected ? Color.FromArgb(0, 120, 215) : Color.FromArgb(240, 240, 240);
            Color foreColor = selected ? Color.White : Color.FromArgb(60, 60, 60);
            FontStyle style = selected ? FontStyle.Bold : FontStyle.Regular;

            using (Brush back = new SolidBrush(backColor))
                e.Graphics.FillRectangle(back, e.Bounds);

            using (Font font = new Font("Yu Gothic UI", 13F, style))
            using (Brush fore = new SolidBrush(foreColor))
            {
                StringFormat sf = new StringFormat
                {
                    Alignment = StringAlignment.Center,
                    LineAlignment = StringAlignment.Center
                };
                e.Graphics.DrawString(page.Text, font, fore, e.Bounds, sf);
            }
        }

        private void SplitContainer_Layout(object sender, LayoutEventArgs e)
        {
            var split = (SplitContainer)sender;
            SetSplitterDistance(split);
        }

        private void MainForm_Shown(object sender, EventArgs e)
        {
            try
            {
                SetSplitterDistance(_focusSplit);
                SetSplitterDistance(_apertureSplit);
                //Text = "フォーカス調整 \u2014 Lens Adjustment Tool";
                _logger.Info("Application start.");
                RestoreApertureRoi();
                ApplySelectedMode(clearMailbox: false);
                _camera.InitializeAndStart(AppDomain.CurrentDomain.BaseDirectory);
                _cameraInfoLabel.Text = _camera.CameraInfo?.ToString() ?? string.Empty;
                _uiTimer.Start();
            }
            catch (Exception ex)
            {
                _logger.Error("Camera initialization failed.", ex);
                _cameraStatusLabel.Text = "Camera: ERROR";
                MessageBox.Show(
                    this,
                    "カメラの初期化に失敗しました。\r\n\r\n" + ex.Message +
                    "\r\n\r\nログ:\r\n" + _logger.LogFilePath,
                    "Camera initialization error",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);
            }
        }

        private void Tabs_SelectedIndexChanged(object sender, EventArgs e)
        {
            ApplySelectedMode(clearMailbox: true);
        }

        private void ApplySelectedMode(bool clearMailbox)
        {
            AdjustmentMode mode = GetSelectedMode();

            _processor.SetMode(mode, resetFocusMaximum: mode == AdjustmentMode.Focus);
            if (clearMailbox)
            {
                _mailbox.Clear();
            }

            if (mode == AdjustmentMode.Focus)
            {
                _focusCurrentValue.Text = "--";
                _focusMaxValue.Text = "--";
            }

            _logger.Info("Mode changed: " + mode);
        }

        private AdjustmentMode GetSelectedMode()
        {
            return _tabs.SelectedTab == _focusTabPage
                ? AdjustmentMode.Focus
                : AdjustmentMode.Aperture;
        }

        private void FocusResetButton_Click(object sender, EventArgs e)
        {
            _processor.ResetFocusMaximum();
            _focusMaxValue.Text = "--";
            _logger.Info("Focus maximum reset.");
        }

        private void ApertureRoiMoveButton_Click(object sender, EventArgs e)
        {
            var button = (Button)sender;
            var delta = (Point)button.Tag;
            Rectangle roi = _processor.MoveApertureCalculationRoi(delta.X, delta.Y);
            UpdateApertureInstructionText(roi);
            SaveApertureRoi(roi);
            _logger.Info($"Aperture ROI moved to ({roi.X}, {roi.Y}).");
        }

        private void RestoreApertureRoi()
        {
            Rectangle? roi = _apertureRoiSettingsStore.Load();
            if (!roi.HasValue)
                return;

            Rectangle applied = _processor.SetApertureCalculationRoi(roi.Value);
            UpdateApertureInstructionText(applied);
            _logger.Info($"Aperture ROI restored to ({applied.X}, {applied.Y}).");
        }

        private void SaveApertureRoi(Rectangle roi)
        {
            try
            {
                _apertureRoiSettingsStore.Save(roi);
            }
            catch (Exception ex)
            {
                _logger.Warn("Failed to save aperture ROI." + Environment.NewLine + ex.Message);
            }
        }

        private void UiTimer_Tick(object sender, EventArgs e)
        {
            _cameraStatusLabel.Text = "Camera: " + _camera.StatusText;

            using (FrameAnalysisResult result = _mailbox.TakeLatest())
            {
                if (result != null)
                {
                    UpdateFromResult(result);
                    _displayedFrameCount++;
                }
            }

            double elapsed = (DateTime.Now - _fpsWindowStart).TotalSeconds;
            if (elapsed >= 1.0)
            {
                double fps = _displayedFrameCount / elapsed;
                _fpsLabel.Text = $"UI: {fps:0.0} fps";
                _displayedFrameCount = 0;
                _fpsWindowStart = DateTime.Now;
            }
        }

        private void UpdateFromResult(FrameAnalysisResult result)
        {
            if (result.Mode == AdjustmentMode.Focus)
            {
                ReplacePicture(_focusPicture, new Bitmap(result.DisplayImage));
                _focusCurrentValue.Text = result.CurrentValue.ToString("0.00");
                _focusMaxValue.Text = result.AuxiliaryValue.ToString("0.00");
            }
            else
            {
                ReplacePicture(_aperturePicture, new Bitmap(result.DisplayImage));
                _apertureCurrentValue.Text = result.CurrentValue.ToString("0.0");
                _apertureDifferenceValue.Text = result.AuxiliaryValue.ToString("+0.0;-0.0;0.0");
            }
        }

        private static void ReplacePicture(PictureBox box, Bitmap next)
        {
            Image old = box.Image;
            box.Image = next;
            old?.Dispose();
        }

        private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (_closing) return;
            _closing = true;

            _uiTimer.Stop();
            try
            {
                _camera.Dispose();
            }
            catch (Exception ex)
            {
                _logger.Error("Error while stopping camera.", ex);
            }

            SaveApertureRoi(_processor.GetApertureCalculationRoi());
            _mailbox.Clear();
            ReplacePicture(_focusPicture, null);
            ReplacePicture(_aperturePicture, null);
            _logger.Info("Application end.");
            _logger.Dispose();
        }
    }
}