/*============================================================================= Project: SharpImage Module: siFormMain.cs Language: C# Author: Dan Mueller Date: $Date: 2008-01-09 16:23:10 +1000 (Wed, 09 Jan 2008) $ Revision: $Revision: 29 $ Copyright (c) Queensland University of Technology (QUT) 2007. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the above copyright notices for more information. =============================================================================*/ using System; using System.IO; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Diagnostics; using System.Runtime.InteropServices; using System.Reflection; using System.Threading; using IronPython.Hosting; using Crownwood.Magic.Common; using Crownwood.Magic.Controls; using SharpImage.Main; using SharpImage.Script; using SharpImage.Forms; using SharpImage.Properties; using SharpImage.Rendering; namespace SharpImage.Main { public partial class siFormMain : Form, IApplication { #region ImageParams and ImageSeriesParams Struct //===================================================================== private class ImageParams { private itk.itkImageBase m_Image; private String m_FullPath; private bool m_Compress; private bool m_Sucess; private Exception m_Exception; public ImageParams(itk.itkImageBase image, String fullPath) { this.m_Image = image; this.m_FullPath = fullPath; this.m_Compress = false; this.m_Sucess = false; this.m_Exception = null; } public ImageParams(itk.itkImageBase image, String fullPath, bool compress) { this.m_Image = image; this.m_FullPath = fullPath; this.m_Compress = compress; this.m_Sucess = false; this.m_Exception = null; } public itk.itkImageBase Image { get { return this.m_Image; } } public String FullPath { get { return this.m_FullPath; } } public bool Compress { get { return this.m_Compress; } set { this.m_Compress = value; } } public bool Success { get { return this.m_Sucess; } set { this.m_Sucess = value; } } public Exception Exception { get { return this.m_Exception; } set { this.m_Exception = value; } } } private class ImageSeriesParams { private itk.itkImageBase m_Image; private String[] m_Files; private bool m_Sucess; private Exception m_Exception; public ImageSeriesParams(itk.itkImageBase image, String[] files) { this.m_Image = image; this.m_Files = files; this.m_Sucess = false; this.m_Exception = null; } public itk.itkImageBase Image { get { return this.m_Image; } } public String[] Files { get { return this.m_Files; } } public bool Success { get { return this.m_Sucess; } set { this.m_Sucess = value; } } public Exception Exception { get { return this.m_Exception; } set { this.m_Exception = value; } } } //===================================================================== #endregion #region ExecuteScriptParams Struct //===================================================================== private struct ExecuteScriptParams { private PythonEngine m_Engine; private string m_FullPath; public ExecuteScriptParams(PythonEngine engine, string fullPath) { this.m_Engine = engine; this.m_FullPath = fullPath; } public PythonEngine Engine { get { return this.m_Engine; } } public string FullPath { get { return this.m_FullPath; } } } //===================================================================== #endregion #region Constants //===================================================================== private const string STATUS_READY = "Ready..."; private const string APPLICATION_TEXT = "SharpImage" + " [" + siVersion.VERSION + "]"; //===================================================================== #endregion #region FormMain Methods //===================================================================== /// /// Default constructor. /// public siFormMain() { InitializeComponent(); // Setup FormMain this.Text = APPLICATION_TEXT; this.Size = new Size(800, 700); this.MenuScriptConsole_Click(this, new EventArgs()); this.HideTabBottom(); this.HideTabRight(); this.SetApplicationAsReady(0); // Watch the tabs collection this.tabBottom.TabPages.Cleared += new Crownwood.Magic.Collections.CollectionClear(TabBottom_TabPages_Cleared); this.tabBottom.TabPages.Removed += new Crownwood.Magic.Collections.CollectionChange(TabPages_Removed); this.tabRight.TabPages.Cleared += new Crownwood.Magic.Collections.CollectionClear(TabRight_TabPages_Cleared); this.tabRight.TabPages.Removed += new Crownwood.Magic.Collections.CollectionChange(TabPages_Removed); // Watch the script console for commands this.ScriptConsole.UserEnteredScriptCommand += new siFormScriptConsole.ScriptCommandHandler(ScriptConsole_UserEnteredScriptCommand); // Add support for recently opened images this.m_RecentOpenImage = new siRecentFileManager("SharpImage", "RecentOpenImage"); this.RefreshRecentOpenImageMenu(); // Add support for recently executed scripts this.m_RecentExecuteScripts = new siRecentFileManager("SharpImage", "RecentExecuteScript"); this.RefreshRecentExecuteScriptsMenu(); // Add a form closing handler to persist entries to registry this.FormClosing += new FormClosingEventHandler(FormMain_FormClosing); } void FormMain_FormClosing(object sender, FormClosingEventArgs e) { // Hide the form so it doesn't look unresponsive while // we persist recent files this.Hide(); // Finish up this.ExitApplication(); } void TabPages_Removed(int index, object value) { if (this.tabBottom.TabPages.Count == 0) this.HideTabBottom(); if (this.tabRight.TabPages.Count == 0) this.HideTabRight(); } private void TabBottom_ClosePressed(object sender, EventArgs e) { if (this.tabBottom.SelectedTab != null) this.tabBottom.TabPages.Remove(this.tabBottom.SelectedTab); } private void TabRight_ClosePressed(object sender, EventArgs e) { if (this.tabRight.SelectedTab != null) { // Try to close the form associated with the page if (this.tabRight.SelectedTab.Tag != null && this.tabRight.SelectedTab.Tag is Form) // NOTE: IApplication.AddToolForm is watching the FormClosed // event and will remove the tab (this.tabRight.SelectedTab.Tag as Form).Close(); else // Remove the page this.tabRight.TabPages.Remove(this.tabRight.SelectedTab); } } void TabBottom_TabPages_Cleared() { this.HideTabBottom(); } void TabRight_TabPages_Cleared() { this.HideTabRight(); } private void ShowTabBottom() { this.splitterBottom.Visible = true; this.tabBottom.Visible = true; } private void ShowTabRight() { this.splitterRight.Visible = true; this.tabRight.Visible = true; } private void HideTabBottom() { this.splitterBottom.Visible = false; this.tabBottom.Visible = false; } private void HideTabRight() { this.tabRight.Visible = false; this.splitterRight.Visible = false; } private void MenuWindowCascade_Click(object sender, EventArgs e) { this.LayoutMdi(MdiLayout.Cascade); } private void MenuWindowTileV_Click(object sender, EventArgs e) { this.LayoutMdi(MdiLayout.TileVertical); } private void MenuWindowTileH_Click(object sender, EventArgs e) { this.LayoutMdi(MdiLayout.TileHorizontal); } private void MenuWindowArrangeIcons_Click(object sender, EventArgs e) { this.LayoutMdi(MdiLayout.ArrangeIcons); } private void menuWindowHideUnused_Click(object sender, EventArgs e) { foreach (siRenderer renderer in this.Renderers) if (renderer.Form != this.ActiveMdiChild) renderer.Hide(); } private void menuWindowShowAll_Click(object sender, EventArgs e) { foreach (siRenderer renderer in this.Renderers) renderer.Form.Show(); } //===================================================================== #endregion #region IApplication Implemenation //===================================================================== #region IApplication Thread-safe Delegates //===================================================================== delegate void IApplicationShowMessageCallback1(string text); delegate void IApplicationShowMessageCallback2(string text, string caption); delegate void IApplicationShowMessageCallback3(string text, string caption, MessageBoxButtons buttons); delegate void IApplicationShowMessageCallback4(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon); delegate void IApplicationSetAsWorkingCallback(); delegate void IApplicationSetProgressCallback(int progress); delegate void IApplicationSetStatusLabelCallback(string status); delegate void IApplicationSetAsReadyCallback(int pause); delegate void IApplicationRendererCallback(siRenderer renderer); delegate siRenderer IApplicationImageCallback(itk.itkImageBase image); delegate void IApplicationAddToolCallback(siFormTool form); delegate itk.itkImageBase IApplicationOpenImageCallback2(bool display, string title); delegate itk.itkImageBase IApplicationOpenImageCallback4(bool display, string title, itk.itkPixelType pixeltype, int dim); //===================================================================== #endregion #region IApplication Properties //===================================================================== private Dictionary m_Metadata = new Dictionary(); public Dictionary Metadata { get { return this.m_Metadata; } } private siFormScriptConsole m_ScriptConsole = null; public siFormScriptConsole ScriptConsole { get { if (this.m_ScriptConsole == null) this.m_ScriptConsole = new siFormScriptConsole(this); return this.m_ScriptConsole; } } public siRenderer CurrentRenderer { get { foreach (siRenderer renderer in this.Renderers) if (this.ActiveMdiChild == renderer.Form) return renderer; if (this.Renderers.Count > 0) return this.Renderers[0]; else return null; } } private List m_Renderers = new List(); public List Renderers { get { return this.m_Renderers; } } //===================================================================== #endregion #region IApplication Methods //===================================================================== /// /// Invoke the given delegate on the application thread. /// /// /// public object InvokeArg0(Delegate method) { return this.Invoke(method); } /// /// Invoke the given delegate on the application thread. /// /// /// /// public object InvokeArg1(Delegate method, object arg1) { return this.Invoke(method, arg1); } /// /// Invoke the given delegate on the application thread. /// /// /// /// /// public object InvokeArg2(Delegate method, object arg1, object arg2) { return this.Invoke(method, arg1, arg2); } /// /// Invoke the given delegate on the application thread. /// /// /// /// /// /// public object InvokeArg3(Delegate method, object arg1, object arg2, object arg3) { return this.Invoke(method, arg1, arg2, arg3); } /// /// Invoke the given delegate on the application thread. /// /// /// /// /// /// /// public object InvokeArg4(Delegate method, object arg1, object arg2, object arg3, object arg4) { return this.Invoke(method, arg1, arg2, arg3, arg4); } /// /// A thread-safe method for showing a message box owned by /// the main IApplication window. /// /// This method is thread-safe. /// public void ShowMessageBox(string text) { // Make the call thread-safe if (this.InvokeRequired) { IApplicationShowMessageCallback1 d = new IApplicationShowMessageCallback1(this.ShowMessageBox); this.Invoke(d, text); return; } // Show the message MessageBox.Show(this, text); } /// /// A thread-safe method for showing a message box owned by /// the main IApplication window. /// /// This method is thread-safe. /// /// public void ShowMessageBox(string text, string caption) { // Make the call thread-safe if (this.InvokeRequired) { IApplicationShowMessageCallback2 d = new IApplicationShowMessageCallback2(this.ShowMessageBox); this.Invoke(d, text, caption); return; } // Show the message MessageBox.Show(this, text, caption); } /// /// A thread-safe method for showing a message box owned by /// the main IApplication window. /// /// This method is thread-safe. /// /// /// public void ShowMessageBox(string text, string caption, MessageBoxButtons buttons) { // Make the call thread-safe if (this.InvokeRequired) { IApplicationShowMessageCallback3 d = new IApplicationShowMessageCallback3(this.ShowMessageBox); this.Invoke(d, text, caption, buttons); return; } // Show the message MessageBox.Show(this, text, caption, buttons); } /// /// A thread-safe method for showing a message box owned by /// the main IApplication window. /// /// This method is thread-safe. /// /// /// /// public void ShowMessageBox(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) { // Make the call thread-safe if (this.InvokeRequired) { IApplicationShowMessageCallback4 d = new IApplicationShowMessageCallback4(this.ShowMessageBox); this.Invoke(d, text, caption, buttons, icon); return; } // Show the message MessageBox.Show(this, text, caption, buttons, icon); } /// /// A thread-safe method for setting the application as working. /// This method sets the cursor. /// /// This method is thread-safe. public void SetApplicationAsWorking() { // Make the call thread-safe if (this.InvokeRequired) { this.Invoke(new IApplicationSetAsWorkingCallback(this.SetApplicationAsWorking)); return; } // Set the cursor this.Cursor = Cursors.WaitCursor; this.ScriptConsole.Cursor = Cursors.WaitCursor; foreach (Form child in this.MdiChildren) child.Cursor = Cursors.WaitCursor; // Force a whole form refresh (to draw over residue menus, etc.) this.Refresh(); } /// /// A thread-safe method for setting the progress indicator. /// The indicator is reset once 100 has been reached. /// /// This method is thread-safe. /// public void SetApplicationProgress(int progress) { // Make the call thread-safe if (this.InvokeRequired) { this.Invoke(new IApplicationSetProgressCallback(this.SetApplicationProgress), progress); return; } // Set progress bar value progress = (progress > this.progressStatus.Maximum) ? this.progressStatus.Maximum : progress; this.progressStatus.Value = progress; // Reset the progressbar (if required) if (progress >= 100) { this.progressStatus.Value = 0; } else if (progress != 0) { // Set the label this.lblStatus.Text = ((string)this.lblStatus.Tag) + progress.ToString("00") + "%"; this.lblStatus.Invalidate(); this.statusStrip.Refresh(); } } /// /// A thread-safe method for setting the status message. /// /// This method is thread-safe. /// public void SetApplicationStatusLabel(string status) { // Ensure the application is still valid if (this == null || this.IsDisposed) return; // Make the call thread-safe if (this.InvokeRequired) { if (!this.IsDisposed) this.Invoke(new IApplicationSetStatusLabelCallback(this.SetApplicationStatusLabel), status); return; } // Set the label this.lblStatus.Text = status; this.lblStatus.Tag = status; this.lblStatus.Invalidate(); this.statusStrip.Refresh(); } /// /// A thread-safe method setting the application as ready. /// This method sets the cursor to the default and resets /// the status label after the given pause. /// /// This method is thread-safe. /// The time (in milliseconds) to wait before resetting the status label. /// If pause less than or equal to 0, the label is reset immediately. public void SetApplicationAsReady(int pause) { // Make the call thread-safe if (this.InvokeRequired) { this.Invoke(new IApplicationSetAsReadyCallback(this.SetApplicationAsReady), pause); return; } // Set the cursor this.Cursor = Cursors.Default; this.ScriptConsole.Cursor = Cursors.Default; foreach (Form child in this.MdiChildren) child.Cursor = Cursors.Default; if (pause > 0) { // Set the status label to READY after the given pause, // only set once (ie. period = Infinite) ParameterizedThreadStart start = new ParameterizedThreadStart(this.SetApplicationAsReadyAfterPause); Thread thread = new Thread(start); thread.Name = "SetApplicationAsReadyAfterPause"; thread.Start(pause); } else { // Set status label immediately this.SetApplicationStatusLabel(STATUS_READY); } } /// /// Sets the status label to "Ready..." after the given pause. /// /// public void SetApplicationAsReadyAfterPause(object oPause) { Thread.Sleep((int)oPause); if (!this.IsDisposed) if (this.lblStatus.Text.Contains("Completed")) this.SetApplicationStatusLabel(STATUS_READY); } /// /// A thread-safe method for showing a Renderer in the /// Application. The Renderer is added as an MDI child, /// and the application watches various Renderer events /// to keep Metadata. /// /// This method is thread-safe. /// public void ShowRenderer(siRenderer renderer) { // Make the call thread-safe if (this.InvokeRequired) { this.Invoke(new IApplicationRendererCallback(this.ShowRenderer), renderer); return; } // Check renderer is not null if (renderer == null) return; // Add the renderer to the Application Metadata if (!this.Renderers.Contains(renderer)) this.Renderers.Add(renderer); // Setup various Application controlled events, etc. renderer.MouseDown += new MouseEventHandler(Renderer_MouseDown); renderer.MouseMove += new MouseEventHandler(Renderer_MouseMove); renderer.Closed += new siRenderer.siRendererHandler(Renderer_Closed); // Show the renderer renderer.Show(this); renderer.Focus(); this.ScriptConsole.Focus(); } /// /// Shows the given image in a new siGdiSliceRenderer. /// /// /// This method is thread-safe. /// /// public siRenderer ShowImageInNewRenderer(itk.itkImageBase image) { // Make the call thread-safe if (this.InvokeRequired) { object result = this.Invoke(new IApplicationImageCallback(this.ShowImageInNewRenderer), image); return result as siRenderer; } // Check the image is not null if (image == null) return null; // Show the image in a new renderer siGdiSliceRenderer newRenderer = new siGdiSliceRenderer( this ); newRenderer.Inputs.Add(image); newRenderer.Initialise(); this.ShowRenderer(newRenderer); return newRenderer; } /// /// A thread-safe method for adding a Tool Form to the Application /// as a tab page. /// /// This method is thread-safe. /// public void AddTool(siFormTool form) { // Make the call thread-safe if (this.InvokeRequired) { this.Invoke(new IApplicationAddToolCallback(this.AddTool), form); return; } // Check form is not null if (form == null) return; // Configure form form.FormClosed += new FormClosedEventHandler(FormTool_FormClosed); Crownwood.Magic.Controls.TabPage page = new Crownwood.Magic.Controls.TabPage(form.Text, form, form.Icon); page.ContextMenu = this.ScriptConsole.ContextMenu; page.Tag = form; this.ShowTabRight(); this.tabRight.TabPages.Add(page); this.tabRight.SelectedTab = page; } private void FormTool_FormClosed(object sender, FormClosedEventArgs e) { this.tabRight.TabPages.Remove(this.tabRight.TabPages[(sender as Form).Text]); } //===================================================================== #endregion //===================================================================== #endregion #region Open Methods //===================================================================== siRecentFileManager m_RecentOpenImage; private void MenuFileOpen_Click(object sender, EventArgs e) { // Open an image and display it. Discard the image object. this.OpenImage(true, null); } private void MenuFileOpenSeries_Click(object sender, EventArgs e) { siFormOpenSeries osd = new siFormOpenSeries(); DialogResult result = osd.ShowDialog(this); if (result == DialogResult.OK) this.OpenImageSeries(osd.PixelType, 3, osd.Files.ToArray()); } private void MenuFileOpenSOMetafile_Click(object sender, EventArgs e) { try { // Setup the dialog siOpenSpatialObjectDialog osod = new siOpenSpatialObjectDialog(); osod.Filter = "Meta Header files (*.mhd)|*.mhd"; osod.FilterIndex = 1; osod.DefaultExt = "mhd"; osod.Title = "Open SpatialObject Metafile..."; DialogResult result = osod.ShowDialog(this); if (result == DialogResult.OK) { // Assume it is a scene itk.itkSpatialObjectReader_F3 reader = itk.itkSpatialObjectReader_F3.New(); reader.FileName = osod.FileName; reader.Update(); siDataObjectDecorator scene = new siDataObjectDecorator(reader.Scene); scene.Metadata["RenderingMethod"] = siSpatialObjectRenderingMethod.Wireframe; scene.Metadata["RenderingSlices"] = 50; scene.Metadata["TubeRenderingMethod"] = siTubeSpatialObjectRenderingMethod.PolyCone; scene.Size = osod.Size; scene.Spacing = osod.Spacing; scene.Origin = osod.Origin; // Create renderer siVolumeRenderer renderer = new siVolumeRenderer(this); renderer.Inputs.Add(scene); renderer.Initialise(); // Create editor siFormRendererEditor editor = new siFormRendererEditor(renderer); // Show everything this.AddTool(editor); this.ShowRenderer(renderer); renderer.Repaint(); } else if (result == DialogResult.Abort) { // Show user message string caption = "Unable to open SpatialObject metafile"; string text = "The size and/or spacing for the SpatialObject was incorrectly specified."; this.ShowMessageBox(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } } catch (Exception ex) { // Show user message string caption = "Unable to open SpatialObject metafile"; string text = "Unable to open SpatialObject metafile.\r\n\r\n" + ex.ToString(); this.ShowMessageBox(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); // Set application as ready this.SetApplicationAsReady(0); } } /// /// Show the open image dialog with the given title. /// The image is read and returned on success. /// /// True and the image is displayed in a default renderer, otherwise the image is not displayed. /// The title of the dialog. If the title is null or empty, the default title is used. /// The opened image or null on cancel/failure. public itk.itkImageBase OpenImage(bool display, string title) { // Make the call thread-safe if (this.InvokeRequired) return (itk.itkImage)this.Invoke(new IApplicationOpenImageCallback2(this.OpenImage), new object[] { display, title } ); // Setup the image dialog siOpenImageDialog oid = new siOpenImageDialog(); oid.Filter = "Meta Header files (*.mhd,*.mha)|*.mhd;*.mha"; oid.Filter += "|PNG Image files (*.png)|*.png"; oid.Filter += "|JPEG Image files (*.jpg)|*.jpg"; oid.Filter += "|BMP Image files (*.bmp)|*.bmp"; oid.Filter += "|TIFF Image files (*.tif)|*.tif"; oid.Filter += "|VTK Image files (*.vtk)|*.vtk"; oid.Filter += "|All Image files (*.mhd,*.mha,*.png,*.jpg,*.bmp,*.tif,*.vtk)|*.mhd;*.mha;*.png;*.jpg;*.bmp;*.tif;*.vtk"; oid.Filter += "|All files (*.*)|*.*"; oid.FilterIndex = 7; oid.DefaultExt = "mhd"; oid.PixelType = itk.itkPixelTypeEnum.UnsignedChar; oid.Dimensions = 2; // Set the dialog title if (title != null && title.Length > 0) oid.Title = title; // Show the dialog if (oid.ShowDialog(this) == DialogResult.OK) { try { // TODO: At the moment, oid.NumberOfComponentsPerPixel returns 0 for variable length vectors itk.itkPixelType pixelType = new itk.itkPixelType(oid.PixelType, oid.PixelArray, oid.NumberOfComponentsPerPixel); return this.OpenImage(display, pixelType, (int)oid.Dimensions, oid.FileName); } catch (Exception ex) { // Show user message string caption = "Unable to open image file"; string text = "Unable to open image file.\r\n\r\n" + ex.ToString(); this.ShowMessageBox(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); // Set application as ready this.SetApplicationAsReady(0); } } // User selected cancel, return null return null; } /// /// Show the open image dialog with the given title, pixeltype and dimension. /// The image is read and returned on success. /// /// True and the image is displayed in a default renderer, otherwise the image is not displayed. /// The title of the dialog. If the title is null or empty, the default title is used. /// The type of the image to open. /// public itk.itkImageBase OpenImage(bool display, string title, itk.itkImageBase image) { if (image == null) return null; else return this.OpenImage(display, title, image.PixelType, (int)image.Dimension); } /// /// Show the open image dialog with the given title, pixeltype and dimension. /// The image is read and returned on success. /// /// True and the image is displayed in a default renderer, otherwise the image is not displayed. /// The title of the dialog. If the title is null or empty, the default title is used. /// The pixel type of the image to open. The pixel type combobox is NOT displayed. /// The dimension of the image to open. The dimension combobox is NOT displayed. /// public itk.itkImageBase OpenImage(bool display, string title, itk.itkPixelType pixelType, int dim) { // Make the call thread-safe if (this.InvokeRequired) return (itk.itkImageBase)this.Invoke(new IApplicationOpenImageCallback4(this.OpenImage), new object[] { display, title, pixelType, dim }); // Setup the image dialog OpenFileDialog ofd = new OpenFileDialog(); ofd.Filter = "Meta Header files (*.mhd,*.mha)|*.mhd;*.mha"; ofd.Filter += "|PNG Image files (*.png)|*.png"; ofd.Filter += "|JPEG Image files (*.jpg)|*.jpg"; ofd.Filter += "|BMP Image files (*.bmp)|*.bmp"; ofd.Filter += "|TIFF Image files (*.tif)|*.tif"; ofd.Filter += "|VTK Image files (*.vtk)|*.vtk"; ofd.Filter += "|All Image files (*.mhd,*.mha,*.png,*.jpg,*.bmp,*.tif,*.vtk)|*.mhd;*.mha;*.png;*.jpg;*.bmp;*.tif;*.vtk"; ofd.FilterIndex = 7; ofd.DefaultExt = "mhd"; // Set the dialog title if (title != null && title.Length > 0) ofd.Title = title; else ofd.Title = "Open image"; // Show the dialog if (ofd.ShowDialog(this) == DialogResult.OK) { return this.OpenImage(display, pixelType, dim, ofd.FileName); } // User selected cancel, return null return null; } private itk.itkImageBase OpenImage(bool display, itk.itkPixelType pixelType, int dim, string fullPath) { // Create the image type itk.itkImageBase image = itk.itkImage.New(pixelType, (uint)dim); // Propagate call return this.OpenImage(display, image, fullPath); } /// /// Open an image in a new renderer from a given path and type string. /// For example: "C:/Temp/test1.png#F2" /// /// The path and type of the image to open. /// public itk.itkImageBase OpenImage(String pathAndTypeString) { // Separate the path and type int index = pathAndTypeString.LastIndexOf("#"); if (index > -1) { String path = pathAndTypeString.Substring(0, index); String imageType = pathAndTypeString.Substring(index + 1); itk.itkImage image = itk.itkImage.New(imageType); return this.OpenImage(true, image, path); } else { throw new ArgumentException("The image path, name, and/or type is invalid. Expecting path/name.ext#type, but received " + pathAndTypeString); } } private itk.itkImageBase OpenImage(bool display, itk.itkImageBase image, String fullPath) { // Setup UI for reading this.SetApplicationStatusLabel("Reading " + Path.GetFileName(fullPath) + "..."); this.SetApplicationAsWorking(); // Start thread ParameterizedThreadStart start = new ParameterizedThreadStart(this.ReadImageWorker); Thread thread = new Thread(start); thread.Name = "ReadImage"; ImageParams p = new ImageParams(image, fullPath); thread.Start(p); // Refresh application, and wait for thread the finish this.Refresh(); thread.Join(); thread = null; // Check result if (!p.Success) { // Show error message string caption = "Unable to open image file"; string text = "Unable to open image file.\r\n\r\n" + p.Exception.ToString(); this.ShowMessageBox(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } else if (image != null && !image.IsDisposed) { // Check we should display the image if (display) { // Create renderer siGdiSliceRenderer renderer = new siGdiSliceRenderer(this); renderer.Inputs.Add(image); renderer.Initialise(); this.ShowRenderer(renderer); // Add to recent files // NOTE: Only add to recent files if we displayed the image this.m_RecentOpenImage.AddFile(p.FullPath + "#" + image.MangledTypeString); this.RefreshRecentOpenImageMenu(); } } // Reset UI this.SetApplicationAsReady(0); // Return the image return image; } void ReadImageWorker(Object oParams) { // Cast to image ImageParams p = oParams as ImageParams; try { // Read image p.Image.Read(p.FullPath); p.Success = true; } catch (Exception ex) { p.Exception = ex; p.Success = false; } } private void OpenImageSeries(itk.itkPixelType pixelType, int dim, string[] files) { // Setup UI for reading this.SetApplicationStatusLabel("Opening image series..."); this.SetApplicationAsWorking(); // Create the image itk.itkImageBase image = itk.itkImage.New(pixelType, (uint)dim); // Start thread ParameterizedThreadStart start = new ParameterizedThreadStart(this.ReadImageSeriesWorker); Thread thread = new Thread(start); thread.Name = "ReadImageSeries"; ImageSeriesParams p = new ImageSeriesParams(image, files); thread.Start(p); // Refresh application, and wait for thread the finish this.Refresh(); thread.Join(); thread = null; // Check result if (!p.Success) { // Show error message string caption = "Unable to open image series"; string text = "Unable to open image series.\r\n\r\n" + p.Exception.ToString(); this.ShowMessageBox(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } else if (image != null && !image.IsDisposed) { // Create renderer siGdiSliceRenderer renderer = new siGdiSliceRenderer(this); renderer.Inputs.Add(image); renderer.Initialise(); this.ShowRenderer(renderer); } // Reset UI this.SetApplicationAsReady(0); } void ReadImageSeriesWorker(Object oParams) { // Cast to image ImageSeriesParams p = oParams as ImageSeriesParams; try { // Read image p.Image.ReadSeries(p.Files); p.Success = true; } catch (Exception ex) { p.Exception = ex; p.Success = false; } } private void RefreshRecentOpenImageMenu() { this.menuFileRecent.DropDownItems.Clear(); int index = 1; for (int i = 0; i < this.m_RecentOpenImage.Count; i++) { siRecentFileManager.siRecentFileEntry entry = this.m_RecentOpenImage[i]; if (entry.FileExists) { ToolStripMenuItem menuFileRecentOpenItem = new ToolStripMenuItem(entry.GetFormattedInfoString(index)); menuFileRecentOpenItem.Tag = entry; menuFileRecentOpenItem.Click += new EventHandler(MenuFileRecentItem_Click); this.menuFileRecent.DropDownItems.Add(menuFileRecentOpenItem); index++; } } this.menuFileRecent.Enabled = (this.menuFileRecent.DropDownItems.Count > 0); } private void MenuFileRecentItem_Click(object sender, EventArgs e) { // Get the recent file info (ie. full path and mangled type string) siRecentFileManager.siRecentFileEntry entry = (siRecentFileManager.siRecentFileEntry)(sender as ToolStripMenuItem).Tag; string caption = "Could not open recent file"; string text = "Could not open recent file: '" + entry.FullInfo + "'."; try { if (entry.FilePath != null && entry.FilePath.Length > 0 && File.Exists(entry.FilePath)) { // Open the image and display it itk.itkImageBase image = itk.itkImage.New(entry.TypeString.TrimStart('I')); this.OpenImage(true, image, entry.FilePath); return; } else { throw new FileNotFoundException("Image file does not exist.", entry.FilePath); } } catch (Exception ex) { // Remove from list this.m_RecentOpenImage.RemoveFile(entry.FullInfo); this.RefreshRecentOpenImageMenu(); // Show warning to user Trace.WriteLine(ex.ToString()); this.ShowMessageBox(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); this.SetApplicationAsReady(0); } } //===================================================================== #endregion #region Close/Exit Methods //===================================================================== private void ExitApplication() { //Persist the recent files this.m_RecentOpenImage.PersistToRegistry(); this.m_RecentOpenImage = null; this.m_RecentExecuteScripts.PersistToRegistry(); this.m_RecentExecuteScripts = null; // Close all renderers siRenderer[] renderers = this.Renderers.ToArray(); foreach (siRenderer renderer in renderers) renderer.Close(); this.Renderers.Clear(); this.m_Renderers = null; // Clear the Metadata this.m_Metadata.Clear(); this.m_Metadata = null; // Clear the script console this.m_ScriptConsole.Clear(); this.m_ScriptConsole.Dispose(); this.m_ScriptConsole = null; // Force an exit try { Application.ExitThread(); } catch (Exception ex) { Trace.WriteLine(ex); } } private void MenuFileClose_Click(object sender, EventArgs e) { if (this.CurrentRenderer != null) this.CurrentRenderer.Close(); } private void MenuFileCloseAll_Click(object sender, EventArgs e) { //Get a copy of all the renderers siRenderer[] renders = this.Renderers.ToArray(); // Close all renderers foreach (siRenderer renderer in renders) renderer.Close(); // Clear the Metadata this.Renderers.Clear(); } private void MenuFileExit_Click(object sender, EventArgs e) { // Close the main form this.Close(); } //===================================================================== #endregion #region Save Methods //===================================================================== private void MenuFileSave_Click(object sender, EventArgs e) { this.SaveImage(); } private void MenuFileSaveAs_Click(object sender, EventArgs e) { this.SaveImageAs(); } /// /// Saves the first input of the current renderer. /// public void SaveImage() { // Ensure we have a Renderer with a valid image if (this.CurrentRenderer == null || this.CurrentRenderer.Inputs.Count == 0 || !(this.CurrentRenderer.Inputs[0] is itk.itkImageBase)) return; // Check the name a valid file path string fullPath = this.CurrentRenderer.Inputs[0].Name; if (File.Exists(fullPath)) this.SaveImageAs(this.CurrentRenderer.Inputs[0] as itk.itkImageBase, fullPath, false); else this.SaveImageAs(); } /// /// Saves the first input of the current renderer to the given path. /// /// public void SaveImageAs(string fullPath) { // Ensure we have a Renderer with a valid image if (this.CurrentRenderer == null || this.CurrentRenderer.Inputs.Count == 0 || !(this.CurrentRenderer.Inputs[0] is itk.itkImageBase)) return; // Save the image this.SaveImageAs(this.CurrentRenderer.Inputs[0] as itk.itkImageBase, fullPath, false); } private void SaveImageAs() { try { // Ensure we have a Renderer with a valid image if (this.CurrentRenderer == null || this.CurrentRenderer.Inputs.Count == 0 || !(this.CurrentRenderer.Inputs[0] is itk.itkImageBase)) return; // Get the image itk.itkImageBase image = this.CurrentRenderer.Inputs[0] as itk.itkImageBase; // Create dialog siSaveImageDialog sid = new siSaveImageDialog(); sid.Filter = "Meta Header files (*.mhd,*.mha)|*.mhd;*.mha"; sid.Filter += "|PNG Image files (*.png)|*.png"; sid.Filter += "|JPEG Image files (*.jpg)|*.jpg"; sid.Filter += "|BMP Image files (*.bmp)|*.bmp"; sid.Filter += "|TIFF Image files (*.tif)|*.tif"; sid.Filter += "|VTK Image files (*.vtk)|*.vtk"; sid.Filter += "|All Image files (*.mhd,*.mha,*.png,*.jpg,*.bmp,*.tif,*.vtk)|*.mhd;*.mha;*.png;*.jpg;*.bmp;*.tif;*.vtk"; sid.FilterIndex = 7; sid.DefaultExt = "mhd"; sid.PixelType = image.PixelType.TypeAsEnum; sid.PixelArray = image.PixelType.ArrayAsEnum; sid.ShowDimensions = false; sid.ShowPixelArray = image.PixelType.IsArray; // Try to extract filename string ext = Path.GetExtension(image.Name); string[] exts = ext.Split(' '); string filename = Path.GetFileNameWithoutExtension(image.Name); if (exts.Length == 1) filename += exts[0]; else if (exts.Length >= 2) filename += "_" + exts[1].Trim('(', ')', '_') + exts[0]; sid.FileName = filename; // Show dialog if (sid.ShowDialog(this) == DialogResult.OK) { // Check if the actual and selected pixel types are the same if (sid.PixelType != image.PixelType.TypeAsEnum || sid.PixelArray != image.PixelType.ArrayAsEnum) { // Not the same: cast to the given type itk.itkImageBase imageWithUserPixelType = itk.itkImage.New(new itk.itkPixelType(sid.PixelType, sid.PixelArray, image.PixelType.NumberOfComponentsPerPixel), image.Dimension); itk.itkImageToImageFilter filterCast; if (sid.PixelArray == itk.itkPixelArrayEnum.Scalar) filterCast = itk.itkCastImageFilter.New(image, imageWithUserPixelType); else filterCast = itk.itkVectorCastImageFilter.New(image, imageWithUserPixelType); filterCast.RemoveAllObservers(); filterCast.SetInput(image); filterCast.Update(); filterCast.GetOutput(imageWithUserPixelType); imageWithUserPixelType.DisconnectPipeline(); (filterCast as IDisposable).Dispose(); filterCast = null; image = imageWithUserPixelType; } // Save this.SaveImageAs(image, sid.FileName, sid.UseCompression); } } catch (Exception ex) { // Show user message string caption = "Unable to save image file"; string text = "Unable to save image file.\r\n\r\n" + ex.ToString(); this.ShowMessageBox(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); // Set application as ready this.SetApplicationAsReady(0); } } private void SaveImageAs(itk.itkImageBase image, string fullPath, bool compress) { // Setup UI for reading this.SetApplicationStatusLabel("Writing " + Path.GetFileName(fullPath) + "..."); this.SetApplicationAsWorking(); // Start thread ParameterizedThreadStart start = new ParameterizedThreadStart(this.WriteImageWorker); Thread thread = new Thread(start); thread.Name = "WriteImage"; ImageParams p = new ImageParams(image, fullPath, compress); thread.Start(p); // Refresh application, and wait for thread the finish this.Refresh(); thread.Join(); thread = null; // Check result if (!p.Success) { // Show error message string caption = "Unable to save image file"; string text = "Unable to save file.\r\n\r\n" + p.Exception.ToString(); this.ShowMessageBox(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } else { // Add to recent files this.m_RecentOpenImage.AddFile(p.FullPath + "#" + (image as itk.INativePointer).MangledTypeString); this.RefreshRecentOpenImageMenu(); // Set the image name image.Name = fullPath; this.Refresh(); } // Reset UI - this does not have to be thread safe this.SetApplicationAsReady(0); } void WriteImageWorker(object oParams) { // Cast to image ImageParams p = (ImageParams)oParams; try { // Write image itk.itkImageFileWriter writer = itk.itkImageFileWriter.New(p.Image); writer.SetInput(p.Image); writer.FileName = p.FullPath; writer.UseCompression = p.Compress; writer.Update(); p.Success = true; } catch (Exception ex) { p.Success = false; p.Exception = ex; } } //===================================================================== #endregion #region Help Methods //===================================================================== private void MenuHelpAbout_Click(object sender, EventArgs e) { siFormAbout about = new siFormAbout(); about.ShowDialog(this); } //===================================================================== #endregion #region Renderer Methods //===================================================================== void Renderer_Closed(siRenderer renderer, EventArgs e) { // Remove from list this.Renderers.Remove(renderer); // Dispose renderer.Dispose(); renderer = null; } void Renderer_MouseMove(object sender, MouseEventArgs e) { // Cast sender to renderer siRenderer renderer = null; if (sender != null && sender is siRenderer) renderer = sender as siRenderer; else return; // Check that renderer has the needed Metadata variables if (!renderer.Metadata.ContainsKey("IsMouseInsideImageSpace") || !renderer.Metadata.ContainsKey("LastImagePointMouseMove") || !renderer.Metadata.ContainsKey("LastImageIndexMouseMove") || !renderer.Metadata.ContainsKey("LastImagePixelMouseMove")) return; // Check the mouse is inside the image bool isMouseInsideImageSpace = (bool)renderer.Metadata["IsMouseInsideImageSpace"]; if (!isMouseInsideImageSpace) this.lblImageLocation.Text = "None"; else { // Show the point, index, and value itk.itkPoint point = (itk.itkPoint)renderer.Metadata["LastImagePointMouseMove"]; itk.itkIndex index = (itk.itkIndex)renderer.Metadata["LastImageIndexMouseMove"]; itk.itkPixel pixel = (itk.itkPixel)renderer.Metadata["LastImagePixelMouseMove"]; // Pass the point through the direction transform itk.itkImageBase input = renderer.Inputs[0] as itk.itkImageBase; point = renderer.DirectionTransformPoint(point); // Show the location information if (point != null && index != null && pixel != null) this.lblImageLocation.Text = index.ToString("000") + " " + point.ToString() + " " + pixel.ToString(); else this.lblImageLocation.Text = "None"; } } void Renderer_MouseDown(object sender, MouseEventArgs e) { if (sender != null && sender is siRenderer) (sender as siRenderer).Focus(); } //===================================================================== #endregion #region IronPython Scripting Methods //===================================================================== siRecentFileManager m_RecentExecuteScripts; private void MenuScriptExecute_Click(object sender, EventArgs e) { OpenFileDialog ofd = new OpenFileDialog(); ofd.CheckFileExists = true; ofd.CheckPathExists = true; ofd.Filter = "IronPython Script files (*.py)|*.py"; ofd.FilterIndex = 0; ofd.Multiselect = false; ofd.RestoreDirectory = true; ofd.ShowHelp = false; ofd.Title = "Please browse the script to execute..."; DialogResult result = ofd.ShowDialog(this); if (result == DialogResult.OK) this.ExecuteScript(ofd.FileName); } private void MenuScriptAbort_Click(object sender, EventArgs e) { // TODO: Abort the script } private void ExecuteScript(string fullPathScript) { // Only execute a script if there is a valid renderer selected if (this.CurrentRenderer == null || this.CurrentRenderer.Inputs.Count == 0) return; // Refresh the Application this.Refresh(); this.SetApplicationAsWorking(); // Add to recent files this.m_RecentExecuteScripts.AddFile(fullPathScript); this.RefreshRecentExecuteScriptsMenu(); // Create a ScriptManager to execute the script siScriptManager manager = new siScriptManager(this, fullPathScript); manager.ScriptFinalised += new siScriptManager.ScriptHandler(Manager_ScriptScriptFinalised); manager.ExecuteScript(); } void ScriptConsole_UserEnteredScriptCommand(string command) { // Refresh the Application this.Refresh(); this.SetApplicationAsWorking(); // Get the current directory // TODO: Allow the user to change the current directory string cd = Application.StartupPath; // Create a manager from the command and execute siScriptManager manager = siScriptManager.CreateFromCommand(this, command, cd); if (manager != null) { manager.ScriptFinalised += new siScriptManager.ScriptHandler(Manager_ScriptScriptFinalised); manager.ExecuteScript(); } else { // Tell the console to re-enter immediate mode this.ScriptConsole.EnterImmediateMode(); } } private void Manager_ScriptScriptFinalised(siScriptManager manager) { // Dispose of the manager manager.Dispose(); manager = null; // Tell the console to re-enter immediate mode this.ScriptConsole.EnterImmediateMode(); } private void RefreshRecentExecuteScriptsMenu() { this.menuScriptRecent.DropDownItems.Clear(); for (int i = 1; i < this.m_RecentExecuteScripts.Count; i++) { siRecentFileManager.siRecentFileEntry entry = this.m_RecentExecuteScripts[i]; ToolStripMenuItem menuScriptRecentItem = new ToolStripMenuItem(entry.GetFormattedInfoString(i)); menuScriptRecentItem.Tag = entry; menuScriptRecentItem.Image = Resources.Script; menuScriptRecentItem.Click += new EventHandler(MenuScriptRecentItem_Click); this.menuScriptRecent.DropDownItems.Add(menuScriptRecentItem); } this.menuScriptRecent.Enabled = (this.menuScriptRecent.DropDownItems.Count > 0); } private void MenuScriptRecentItem_Click(object sender, EventArgs e) { // Get the recent script full path siRecentFileManager.siRecentFileEntry entry = (siRecentFileManager.siRecentFileEntry)(sender as ToolStripMenuItem).Tag; if (File.Exists(entry.FilePath)) this.ExecuteScript(entry.FilePath); else { // Show warning to user string caption = "Could not execute recent script"; string text = "Could not execute recent script: '" + entry.FilePath + "'."; this.ShowMessageBox(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); // Remove from list this.m_RecentExecuteScripts.RemoveFile(entry.FullInfo); this.RefreshRecentExecuteScriptsMenu(); } } private void MenuScriptConsole_Click(object sender, EventArgs e) { if (this.tabBottom.TabPages["Script Console"] == null) { // Add console to tabs Crownwood.Magic.Controls.TabPage page = new Crownwood.Magic.Controls.TabPage("Script Console", ScriptConsole, ScriptConsole.Icon); page.ContextMenu = this.ScriptConsole.ContextMenu; this.tabBottom.TabPages.Add(page); } // Toggle bottom tab if (this.tabBottom.Visible) { this.HideTabBottom(); } else { this.ShowTabBottom(); this.ScriptConsole.Focus(); } } //===================================================================== #endregion #region DEBUG: Test Methods //===================================================================== #if DEBUG private void menuTextureRendering_Click(object sender, EventArgs e) { try { // Open a test image itk.itkImage_UC3 imageValue = itk.itkImage_UC3.New(); imageValue.Read(@"C:\DanMueller\Data\VolVis\engine.mhd"); imageValue.Metadata["Value"] = true; itk.itkImage_UC3 imageGradMag = itk.itkImage_UC3.New(); imageGradMag.Read(@"C:\DanMueller\Data\VolVis\engine_GradientMagnitude.mhd"); imageGradMag.Metadata["GradientMagnitude"] = true; // Create transfer function itk.itkSize size = new itk.itkSize(256, 256); string[] layers = { "Layer1", "Layer2" }; siTransferFunction tf = new siTransferFunction(size, layers); siFormTransferFunctionEditor editorTf = new siFormTransferFunctionEditor(tf); // Create renderer siVolumeRenderer renderer = new siVolumeRenderer(this); renderer.Closed += new siRenderer.siRendererHandler(editorTf.OnRendererClosed); renderer.Metadata["VertexProgram"] = @"C:\Utils\SharpImage\Source\Rendering\VolumeRendering\shader-vm.vert"; renderer.Metadata["FragmentProgram"] = @"C:\Utils\SharpImage\Source\Rendering\VolumeRendering\shader-vm.frag"; renderer.Metadata["TransferFunction"] = tf; renderer.Inputs.Add(imageValue); renderer.Inputs.Add(imageGradMag); renderer.Initialise(); // Create renderer editor siFormRendererEditor editorRenderer = new siFormRendererEditor(renderer); // Show everything this.AddTool(editorTf); this.AddTool(editorRenderer); this.ShowRenderer(renderer); } catch (Exception ex) { Trace.WriteLine(ex); } } private int m_Iteration = 0; void filter_Iteration(itk.itkObject sender) { this.SetApplicationProgress(m_Iteration++); } void filter_Progress(itk.itkProcessObject sender, float progress) { this.SetApplicationProgress((int)(progress * 100.0F)); } void filter_Ended(itk.itkObject sender, DateTime time) { this.ScriptConsole.WriteLine((sender as itk.itkProcessObject).Name + " Finished: " + time.ToString()); } void filter_Started(itk.itkObject sender, DateTime time) { this.ScriptConsole.WriteLine((sender as itk.itkProcessObject).Name + " Started: " + time.ToString()); } private void menuTransferFunction_Click(object sender, EventArgs e) { // Open background itk.itkImage_UC2 back = itk.itkImage_UC2.New(); back.Read("C:/Temp/C_ValueEdgeHistogram.png"); itk.itkSize size = new itk.itkSize(512, 128); string[] arrayLayers = { "Context", "Heart", "Arteries" }; List layers = new List(arrayLayers); siTransferFunction tf = new siTransferFunction(size, layers); tf.BackgroundImage = back; tf.Modified += new siTransferFunction.siTransferFunctionModifiedHandler(tf_Modified); siFormTransferFunctionEditor editor = new siFormTransferFunctionEditor(tf); (this as IApplication).AddTool(editor); } void tf_Modified(siTransferFunction sender, bool partial) { //sender.PaintToImage(); //sender.Image.Write("C:/temp/Test1.mhd"); } private void menuTubeRendering_Click(object sender, EventArgs e) { //// Open a test image //itk.itkImage_UC3 imageValue = itk.itkImage_UC3.New(); //imageValue.Read(@"C:\temp\engine.mhd"); //imageValue.Metadata["Value"] = true; //itk.itkImage_UC3 imageGradMag = itk.itkImage_UC3.New(); //imageGradMag.Read(@"C:\temp\engine_GradientMagnitude.mhd"); //imageGradMag.Metadata["GradientMagnitude"] = true; //// Create transfer function //itk.itkSize size = new itk.itkSize(256, 256); //string[] layers = { "Layer1", "Layer2" }; //siTransferFunction tf = new siTransferFunction(size, layers); //siFormTransferFunctionEditor editorTf = new siFormTransferFunctionEditor(tf); //// Open spatial object scene //siSceneSpatialObject scene = siSceneSpatialObject.New(); //scene.Metadata["TubeRenderingMethod"] = siTubeSpatialObjectRenderingMethod.PolyCone; //scene.Metadata["TubeRenderingStyle"] = siTubeSpatialObjectRenderingStyle.Surface; //scene.Metadata["RenderingSlices"] = 50; //scene.Read("C:/Temp/Test2.mhd"); //scene.Size = imageValue.Size; //scene.Spacing = imageValue.Spacing; //scene.Origin = imageValue.Origin; //// Create renderer //siVolumeRenderer renderer = new siVolumeRenderer(this); //renderer.Inputs.Add(imageValue); //renderer.Inputs.Add(imageGradMag); //renderer.Inputs.Add(scene); //renderer.Metadata["TransferFunction"] = tf; //renderer.Metadata["VertexProgram"] = @"C:\Dan\SVN_WORK\Software\ITK\SharpImage\trunk\Source\Rendering\VolumeRendering\shader-vm.vert"; //renderer.Metadata["FragmentProgram"] = @"C:\Dan\SVN_WORK\Software\ITK\SharpImage\trunk\Source\Rendering\VolumeRendering\shader-vm.frag"; //renderer.Initialise(); //renderer.Closed += editorTf.OnRendererClosed; //// Create renderer editor //siFormRendererEditor editorRenderer = new siFormRendererEditor(renderer); //// Show everything //this.AddTool(editorTf); //this.AddTool(editorRenderer); //this.ShowRenderer(renderer); } #endif //===================================================================== #endregion } }