558 lines
23 KiB
C#
558 lines
23 KiB
C#
using System.Diagnostics;
|
|
using System.Drawing.Imaging;
|
|
using System.Reflection;
|
|
using System.Reflection.PortableExecutable;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using Core;
|
|
using Desktop.Audio;
|
|
|
|
namespace Desktop
|
|
{
|
|
public partial class ParsonsForm1 : Form
|
|
{
|
|
public SmsMachine _machine = null!;
|
|
private DebuggerForm _debugger;
|
|
private VramViewerForm _vramViewer;
|
|
private Bitmap _screenBitmap = new Bitmap(256, 192, PixelFormat.Format32bppArgb);
|
|
private NAudioPlayer _audioPlayer;
|
|
private Task _emulatorTask;
|
|
public int TotalFrameCount = 0;
|
|
public double FrameTime { get; private set; } = 0;
|
|
public double FramesPerSecond { get; private set; } = 0;
|
|
private string _currentRomName = "No ROM";
|
|
private string _currentRomPath = "";
|
|
private Stopwatch _stopwatch = new Stopwatch();
|
|
private string _romType = "TBC";
|
|
private bool isTurboMode = false;
|
|
public bool IsRunning { get; private set; } = false;
|
|
|
|
public ushort? Breakpoint
|
|
{
|
|
get => _machine?.Breakpoint;
|
|
set { if (_machine != null) _machine.Breakpoint = value; }
|
|
}
|
|
|
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
|
public ParsonsForm1()
|
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
|
{
|
|
InitializeComponent();
|
|
this.KeyPreview = true;
|
|
#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
|
|
this.KeyDown += Form1_KeyDown;
|
|
#pragma warning restore CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
|
|
#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
|
|
this.KeyUp += Form1_KeyUp;
|
|
#pragma warning restore CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
|
|
this.DoubleBuffered = true;
|
|
this.ResizeRedraw = true;
|
|
}
|
|
protected override void OnLoad(EventArgs e)
|
|
{
|
|
base.OnLoad(e);
|
|
|
|
this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName}";
|
|
|
|
this.BackColor = Color.Black;
|
|
_machine = new SmsMachine();
|
|
_audioPlayer = new NAudioPlayer();
|
|
_machine.AudioProcessor.AudioDevice = _audioPlayer;
|
|
IsRunning = false;
|
|
|
|
PopulateIncludedRomsMenu();
|
|
}
|
|
|
|
private void DrawScreen(int[] safeFrame)
|
|
{
|
|
// Rapidly the Frame into the Windows Bitmap
|
|
var data = _screenBitmap.LockBits(new Rectangle(0, 0, 256, 192), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
|
|
|
|
//Frame buffer draw
|
|
Marshal.Copy(safeFrame, 0, data.Scan0, safeFrame.Length);
|
|
|
|
//Direct VDP draw
|
|
//Marshal.Copy(_machine.VideoProcessor.FrameBuffer, 0, data.Scan0, _machine.VideoProcessor.FrameBuffer.Length);
|
|
|
|
_screenBitmap.UnlockBits(data);
|
|
this.Invalidate();
|
|
TotalFrameCount++;
|
|
}
|
|
|
|
protected override void OnPaint(PaintEventArgs e)
|
|
{
|
|
base.OnPaint(e);
|
|
|
|
if (_screenBitmap != null && menuStrip1 != null)
|
|
{
|
|
// 1. Maintain perfect, chunky retro pixels
|
|
e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
|
|
e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;
|
|
|
|
int topOffset = menuStrip1.Height;
|
|
int availableWidth = this.ClientSize.Width;
|
|
int availableHeight = this.ClientSize.Height - topOffset;
|
|
|
|
// 2. THE LENS LENS: Determine what part of the VDP buffer we actually want to show!
|
|
// (Safely check if the machine/video processor exists yet)
|
|
bool isGG = _machine?.VideoProcessor?.IsGameGear ?? false;
|
|
|
|
int sourceWidth = isGG ? 160 : 256;
|
|
int sourceHeight = isGG ? 144 : 192;
|
|
int sourceX = isGG ? 48 : 0; // Skip 48 pixels in!
|
|
int sourceY = isGG ? 24 : 0; // Skip 24 pixels down!
|
|
|
|
// Create a rectangle defining the specific pixels to extract from the raw frame buffer
|
|
Rectangle sourceRect = new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight);
|
|
|
|
// 3. Calculate the maximum scale factor for the cropped image
|
|
float scaleX = (float)availableWidth / sourceWidth;
|
|
float scaleY = (float)availableHeight / sourceHeight;
|
|
|
|
// Pick the smaller scale so the image never bleeds off the edge
|
|
float scale = Math.Min(scaleX, scaleY);
|
|
|
|
// 4. Calculate the new physical pixel dimensions
|
|
int newWidth = (int)(sourceWidth * scale);
|
|
int newHeight = (int)(sourceHeight * scale);
|
|
|
|
// 5. Center the image (Letterboxing / Pillarboxing)
|
|
int offsetX = (availableWidth - newWidth) / 2;
|
|
int offsetY = topOffset + ((availableHeight - newHeight) / 2);
|
|
|
|
// Create a rectangle defining exactly where on the Windows form to draw the extracted pixels
|
|
Rectangle destRect = new Rectangle(offsetX, offsetY, newWidth, newHeight);
|
|
|
|
// 6. Blast the perfectly cropped and scaled bitmap onto the screen!
|
|
e.Graphics.DrawImage(_screenBitmap, destRect, sourceRect, GraphicsUnit.Pixel);
|
|
}
|
|
}
|
|
|
|
|
|
public void StartEmulator()
|
|
{
|
|
if (IsRunning) return;
|
|
IsRunning = true;
|
|
TotalFrameCount = 0;
|
|
_emulatorTask = Task.Run(() =>
|
|
{
|
|
double targetFps = 59.94;
|
|
double TargetFrameTime = 1000.0 / targetFps;
|
|
_stopwatch.Restart();
|
|
|
|
double nextFrameTargetTime = _stopwatch.Elapsed.TotalMilliseconds + TargetFrameTime;
|
|
double lastFpsUpdate = _stopwatch.Elapsed.TotalMilliseconds;
|
|
int framesThisSecond = 0;
|
|
try
|
|
{
|
|
while (IsRunning)
|
|
{
|
|
//targetFps = isTurboMode ? 1000 : 59.94;
|
|
//TargetFrameTime = 1000.0 / targetFps;
|
|
BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
|
{
|
|
targetFps = isTurboMode ? 1000 : 59.94;
|
|
TargetFrameTime = 1000.0 / targetFps;
|
|
});
|
|
double frameStartTime = _stopwatch.Elapsed.TotalMilliseconds;
|
|
|
|
// --- POLL PHYSICAL CONTROLLER ---
|
|
if (XInput.XInputGetState(0, out XInput.XINPUT_STATE state) == 0)
|
|
{
|
|
ushort btns = state.Gamepad.wButtons;
|
|
byte padState = 0xFF;
|
|
|
|
if ((btns & 0x0001) != 0) padState &= 0xFE; // Up
|
|
if ((btns & 0x0002) != 0) padState &= 0xFD; // Down
|
|
if ((btns & 0x0004) != 0) padState &= 0xFB; // Left
|
|
if ((btns & 0x0008) != 0) padState &= 0xF7; // Right
|
|
if ((btns & 0x1000) != 0) padState &= 0xEF; // Button 1
|
|
if ((btns & 0x2000) != 0) padState &= 0xDF; // Button 2
|
|
|
|
// THE FIX: Constantly update the gamepad state, even when it's 0xFF!
|
|
_machine.IoBus.Joypad1Gamepad = padState;
|
|
}
|
|
// --------------------------------
|
|
_machine.RunFrame();
|
|
|
|
int[] frozenFrame = (int[])_machine.VideoProcessor.FrameBuffer.Clone();
|
|
|
|
// 2. FIRE AND FORGET! Tell Windows to draw, but DO NOT WAIT for it to finish!
|
|
BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { DrawScreen(frozenFrame); });
|
|
|
|
// 3. Safety catch
|
|
if (_machine.Breakpoint.HasValue && _machine.Cpu.PC == _machine.Breakpoint.Value)
|
|
{
|
|
IsRunning = false;
|
|
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
|
|
BeginInvoke((System.Windows.Forms.MethodInvoker)delegate { _debugger?.uiUpdateTimer_Tick(null, EventArgs.Empty); });
|
|
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
|
|
break;
|
|
}
|
|
|
|
// 4. Calculate TRUE Frame Time (Compute Time) BEFORE we go to sleep!
|
|
FrameTime = _stopwatch.Elapsed.TotalMilliseconds - frameStartTime;
|
|
|
|
// --- HIGH PRECISION THROTTLE ---
|
|
while (_stopwatch.Elapsed.TotalMilliseconds < nextFrameTargetTime)
|
|
{
|
|
if (nextFrameTargetTime - _stopwatch.Elapsed.TotalMilliseconds > 2.0)
|
|
{
|
|
Thread.Sleep(1);
|
|
}
|
|
else
|
|
{
|
|
Thread.SpinWait(10);
|
|
}
|
|
}
|
|
|
|
double currentTime = _stopwatch.Elapsed.TotalMilliseconds;
|
|
TotalFrameCount++;
|
|
framesThisSecond++;
|
|
|
|
if (currentTime - lastFpsUpdate >= 1000.0)
|
|
{
|
|
FramesPerSecond = framesThisSecond / ((currentTime - lastFpsUpdate) / 1000.0);
|
|
framesThisSecond = 0;
|
|
lastFpsUpdate = currentTime;
|
|
|
|
BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
|
{
|
|
this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName} [FPS: {FramesPerSecond:F0}]";
|
|
});
|
|
}
|
|
|
|
nextFrameTargetTime += TargetFrameTime;
|
|
|
|
if (currentTime > nextFrameTargetTime + TargetFrameTime)
|
|
{
|
|
nextFrameTargetTime = currentTime + TargetFrameTime;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
IsRunning = false;
|
|
|
|
this.Invoke((System.Windows.Forms.MethodInvoker)delegate
|
|
{
|
|
MessageBox.Show(
|
|
$"The Z80 CPU Panicked!\n\n" +
|
|
$"PC Address: 0x{_machine.Cpu.PC:X4}\n" +
|
|
$"Error: {ex.Message}\n\n" +
|
|
$"Stack Trace:\n{ex.StackTrace}",
|
|
"Fatal Emulator Crash",
|
|
MessageBoxButtons.OK,
|
|
MessageBoxIcon.Error);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
public void StopEmulator()
|
|
{
|
|
IsRunning = false;
|
|
}
|
|
|
|
private async void LoadRomDataAndStart(byte[] romData, string fileName, string uniqueId)
|
|
{
|
|
StopEmulator();
|
|
if (_emulatorTask != null)
|
|
{
|
|
await _emulatorTask;
|
|
}
|
|
|
|
SaveCurrentSram();
|
|
|
|
// Blast the bytes straight into the virtual cartridge slot!
|
|
_machine.LoadCartridge(romData);
|
|
|
|
// Check the hardware mode based on the file name
|
|
bool isGG = fileName.EndsWith(".gg", StringComparison.OrdinalIgnoreCase);
|
|
_machine.VideoProcessor.IsGameGear = isGG;
|
|
|
|
// We use uniqueId to track the SRAM file path (works for both real paths and resource names)
|
|
_currentRomPath = uniqueId;
|
|
_currentRomName = Path.GetFileNameWithoutExtension(fileName);
|
|
|
|
// Update the window title dynamically
|
|
_romType = isGG ? "GG" : "MS";
|
|
this.Text = $"{_romType} Parsons Master System 2026 - {_currentRomName}";
|
|
|
|
string savPath = GetSaveFilePath();
|
|
if (savPath != null)
|
|
{
|
|
_machine.MemoryBus.LoadSaveData(savPath);
|
|
}
|
|
|
|
StartEmulator();
|
|
}
|
|
|
|
private void LoadRomAndStart(string filePath)
|
|
{
|
|
if (!File.Exists(filePath)) return;
|
|
|
|
// Read from the hard drive, then pass to the universal loader
|
|
byte[] romData = File.ReadAllBytes(filePath);
|
|
LoadRomDataAndStart(romData, Path.GetFileName(filePath), filePath);
|
|
}
|
|
|
|
|
|
private string GetSaveFilePath()
|
|
{
|
|
// Don't try to save if a game hasn't been loaded yet!
|
|
if (string.IsNullOrEmpty(_currentRomName) || _currentRomName == "No ROM")
|
|
#pragma warning disable CS8603 // Possible null reference return.
|
|
return null;
|
|
#pragma warning restore CS8603 // Possible null reference return.
|
|
|
|
// Application.StartupPath is the exact folder containing your .exe
|
|
string exeFolder = Application.StartupPath;
|
|
|
|
// Combines the exe folder with "GameName.sav"
|
|
return Path.Combine(exeFolder, _currentRomName + ".sav");
|
|
}
|
|
private void SaveCurrentSram()
|
|
{
|
|
if (_machine != null)
|
|
{
|
|
string savPath = GetSaveFilePath();
|
|
if (savPath != null)
|
|
{
|
|
_machine.MemoryBus.SaveSaveData(savPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void PopulateIncludedRomsMenu()
|
|
{
|
|
string romsDirectory = "Desktop.ROMS";
|
|
string[] files = GetFilesInResourceDirectory(romsDirectory);
|
|
|
|
foreach (string resourceName in files)
|
|
{
|
|
// 1. Strip the "Desktop.ROMS." prefix
|
|
string cleanFileName = resourceName.Replace("Desktop.ROMS.", "");
|
|
|
|
// 2. Strip the extension for a beautiful menu item (e.g. "Sonic")
|
|
string menuName = Path.GetFileNameWithoutExtension(cleanFileName);
|
|
|
|
ToolStripMenuItem romMenuItem = new ToolStripMenuItem(menuName);
|
|
|
|
// 3. Point the click event to our new extraction helper!
|
|
romMenuItem.Click += (sender, e) => LoadEmbeddedRom(resourceName);
|
|
|
|
// 4. Sort into the correct menu based on extension!
|
|
if (resourceName.EndsWith(".gg", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
gameGearToolStripMenuItem.DropDownItems.Add(romMenuItem);
|
|
}
|
|
else if (resourceName.EndsWith(".sms", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
masterSystemToolStripMenuItem.DropDownItems.Add(romMenuItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void LoadEmbeddedRom(string resourceName)
|
|
{
|
|
// 1. Strip off the ugly prefix to get the real file name for the UI and the .gg check
|
|
string cleanFileName = resourceName.Replace("Desktop.ROMS.", "");
|
|
|
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
|
|
|
// 2. Open a direct pipe to the internal resource
|
|
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
|
|
{
|
|
if (stream == null) return;
|
|
|
|
// 3. Copy the stream entirely in RAM—never touching the hard drive!
|
|
using (MemoryStream ms = new MemoryStream())
|
|
{
|
|
stream.CopyTo(ms);
|
|
byte[] romData = ms.ToArray();
|
|
|
|
// 4. Pass the raw bytes to our universal loader
|
|
LoadRomDataAndStart(romData, cleanFileName, resourceName);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static string[] GetFilesInResourceDirectory(string directoryPrefix)
|
|
{
|
|
// Get the assembly containing the embedded resources.
|
|
// Change this if your resources are in a different project/assembly.
|
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
|
|
|
// Ensure the prefix ends with a dot so we don't accidentally match
|
|
// similarly named folders (e.g., matching "Folder" but not "FolderTwo")
|
|
if (!directoryPrefix.EndsWith("."))
|
|
{
|
|
directoryPrefix += ".";
|
|
}
|
|
|
|
// Get all resource names and filter by our "directory" prefix
|
|
string[] allResources = assembly.GetManifestResourceNames();
|
|
|
|
string[] filesInDirectory = allResources
|
|
.Where(resource => resource.StartsWith(directoryPrefix, StringComparison.OrdinalIgnoreCase))
|
|
.ToArray();
|
|
|
|
return filesInDirectory;
|
|
}
|
|
|
|
private void selectROMToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
using (OpenFileDialog ofd = new OpenFileDialog())
|
|
{
|
|
ofd.Title = "Select Master System ROM";
|
|
ofd.Filter = "SMS ROMs (*.sms)|*.sms|GG ROMs (*.gg)|*.gg|All Files (*.*)|*.*";
|
|
|
|
if (ofd.ShowDialog() == DialogResult.OK)
|
|
{
|
|
LoadRomAndStart(ofd.FileName);
|
|
_machine.Reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void resetToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
_machine.Reset();
|
|
}
|
|
|
|
private void debuggerToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
if (_debugger == null || _debugger.IsDisposed)
|
|
{
|
|
_debugger = new DebuggerForm(_machine.Cpu, _machine.MemoryBus, this);
|
|
}
|
|
_debugger.Show();
|
|
}
|
|
|
|
private void vramViewerToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
if (_vramViewer == null || _vramViewer.IsDisposed)
|
|
{
|
|
// Pass the VDP directly to the viewer!
|
|
_vramViewer = new VramViewerForm(_machine.VideoProcessor);
|
|
}
|
|
_vramViewer.Show();
|
|
}
|
|
|
|
|
|
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
this.Close();
|
|
}
|
|
|
|
private void Form1_KeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
UpdateJoypad(e.KeyCode, true);
|
|
}
|
|
|
|
private void Form1_KeyUp(object sender, KeyEventArgs e)
|
|
{
|
|
UpdateJoypad(e.KeyCode, false);
|
|
}
|
|
|
|
private void UpdateJoypad(Keys key, bool isPressed)
|
|
{
|
|
if (_machine == null) return;
|
|
|
|
byte bitMask = 0;
|
|
|
|
// Map your keys to the Sega hardware bits
|
|
switch (key)
|
|
{
|
|
case Keys.W: bitMask = 0x01; break; // Bit 0: Up
|
|
case Keys.S: bitMask = 0x02; break; // Bit 1: Down
|
|
case Keys.A: bitMask = 0x04; break; // Bit 2: Left
|
|
case Keys.D: bitMask = 0x08; break; // Bit 3: Right
|
|
case Keys.O: bitMask = 0x10; break; // Bit 4: Button 1 (Start/Action)
|
|
case Keys.P: bitMask = 0x20; break; // Bit 5: Button 2
|
|
default: return; // Ignore any other keys
|
|
}
|
|
|
|
if (isPressed)
|
|
{
|
|
_machine.IoBus.Joypad1Keyboard &= (byte)~bitMask; // Target Keyboard!
|
|
}
|
|
else
|
|
{
|
|
_machine.IoBus.Joypad1Keyboard |= bitMask; // Target Keyboard!
|
|
}
|
|
}
|
|
|
|
private void ParsonsForm1_FormClosing(object sender, FormClosingEventArgs e)
|
|
{
|
|
IsRunning = false;
|
|
SaveCurrentSram();
|
|
_audioPlayer?.Stop();
|
|
}
|
|
private async void saveStateToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
if (string.IsNullOrEmpty(_currentRomName) || _currentRomName == "No ROM") return;
|
|
|
|
// 1. Politely ask the emulator loop to pause, and wait for it to finish its current frame!
|
|
StopEmulator();
|
|
if (_emulatorTask != null) await _emulatorTask;
|
|
|
|
// 2. Change the extension to .state and save it right next to the .exe
|
|
string statePath = Path.ChangeExtension(GetSaveFilePath(), ".state");
|
|
_machine.SaveState(statePath);
|
|
|
|
// 3. Resume the emulator seamlessly
|
|
StartEmulator();
|
|
}
|
|
|
|
private async void loadStateToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
if (string.IsNullOrEmpty(_currentRomName) || _currentRomName == "No ROM") return;
|
|
|
|
string statePath = Path.ChangeExtension(GetSaveFilePath(), ".state");
|
|
if (!File.Exists(statePath)) return;
|
|
|
|
// 1. Pause the emulator
|
|
StopEmulator();
|
|
if (_emulatorTask != null) await _emulatorTask;
|
|
|
|
// 2. Inject the frozen state into the silicon!
|
|
_machine.LoadState(statePath);
|
|
|
|
// 3. Resume
|
|
StartEmulator();
|
|
}
|
|
|
|
private void turboModeToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
turboModeToolStripMenuItem.Checked = !turboModeToolStripMenuItem.Checked;
|
|
isTurboMode = turboModeToolStripMenuItem.Checked ? true : false;
|
|
}
|
|
|
|
private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
MessageBox.Show(
|
|
$"First full release of Parsons Master System Emulator 2026!\r\n\r\nFeatures (v1.0):\r\n\r\n" +
|
|
$" *Full compatibility with all Master System and Game Gear ROMs\r\n" +
|
|
$" *Full window scaling with fixed aspect ratio (SMS & GG)\r\n" +
|
|
$" *Full 4 channel SN76489 implememtation using NAudio\r\n" +
|
|
$" *SRAM auto save and load (Battery Backed RAM)\r\n" +
|
|
$" *Keyboard and XInput control pad support\r\n" +
|
|
$" *Comprehensive Debugger\r\n" +
|
|
$" *VRAM Viewer\r\n" +
|
|
$" *Single point Save State\r\n" +
|
|
$" *Turbo mode\r\n" +
|
|
$" *Includes a selection of commercial ROMs\r\n\r\n" +
|
|
$"Planned updates (v1.1):\r\n\r\n" +
|
|
$" *Architectural double buffering (prevent screen tear)\r\n" +
|
|
$" *Graphics shaders (scanlines etc...)\r\n" +
|
|
$" *Add multiple Save States per game\r\n\r\n\r\n" +
|
|
$"(c) Marc Parsons 2026","Parsons Master System 2026 v1.0",
|
|
MessageBoxButtons.OK,
|
|
MessageBoxIcon.Information);
|
|
}
|
|
}
|
|
}
|