Files
mbn_stock/mbn_stock/Form1.cs

349 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.Linq;
using System.Windows.Forms;
namespace mbn_stock
{
public partial class Form1 : Form
{
private readonly BindingList<SectorHeatMapItem> _items = new BindingList<SectorHeatMapItem>();
private readonly Random _random = new Random();
private DataGridView _grid;
private PictureBox _previewBox;
private Button _randomButton;
private Button _saveButton;
private Label _statusLabel;
private bool _loading;
public Form1()
{
InitializeComponent();
BuildUi();
LoadDefaultItems();
RandomizeValues();
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
if (_previewBox != null && _previewBox.Image != null)
{
_previewBox.Image.Dispose();
_previewBox.Image = null;
}
base.OnFormClosed(e);
}
private void BuildUi()
{
Font = new Font("Malgun Gothic", 9.5f, FontStyle.Regular, GraphicsUnit.Point);
BackColor = Color.FromArgb(245, 246, 248);
MinimumSize = new Size(1100, 700);
SplitContainer splitContainer = new SplitContainer();
splitContainer.Dock = DockStyle.Fill;
splitContainer.FixedPanel = FixedPanel.Panel1;
splitContainer.SplitterDistance = 500;
splitContainer.Panel1MinSize = 420;
splitContainer.Panel2MinSize = 520;
splitContainer.BackColor = Color.FromArgb(210, 214, 220);
Panel leftPanel = new Panel();
leftPanel.Dock = DockStyle.Fill;
leftPanel.Padding = new Padding(14);
leftPanel.BackColor = Color.White;
FlowLayoutPanel toolbar = new FlowLayoutPanel();
toolbar.Dock = DockStyle.Top;
toolbar.Height = 46;
toolbar.FlowDirection = FlowDirection.LeftToRight;
toolbar.WrapContents = false;
_randomButton = new Button();
_randomButton.Text = "랜덤 값";
_randomButton.Width = 110;
_randomButton.Height = 32;
_randomButton.Click += RandomButton_Click;
_saveButton = new Button();
_saveButton.Text = "PNG 저장";
_saveButton.Width = 110;
_saveButton.Height = 32;
_saveButton.Click += SaveButton_Click;
toolbar.Controls.Add(_randomButton);
toolbar.Controls.Add(_saveButton);
_grid = new DataGridView();
_grid.Dock = DockStyle.Fill;
_grid.AutoGenerateColumns = false;
_grid.AllowUserToAddRows = false;
_grid.AllowUserToDeleteRows = false;
_grid.AllowUserToResizeRows = false;
_grid.RowHeadersVisible = false;
_grid.SelectionMode = DataGridViewSelectionMode.CellSelect;
_grid.MultiSelect = false;
_grid.BackgroundColor = Color.White;
_grid.BorderStyle = BorderStyle.FixedSingle;
_grid.EnableHeadersVisualStyles = false;
_grid.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(238, 241, 245);
_grid.ColumnHeadersDefaultCellStyle.ForeColor = Color.FromArgb(30, 35, 45);
_grid.ColumnHeadersDefaultCellStyle.Font = new Font(Font, FontStyle.Bold);
_grid.DefaultCellStyle.SelectionBackColor = Color.FromArgb(218, 232, 255);
_grid.DefaultCellStyle.SelectionForeColor = Color.FromArgb(20, 24, 32);
_grid.DataError += Grid_DataError;
_grid.CellEndEdit += Grid_CellEndEdit;
_grid.CellParsing += Grid_CellParsing;
_grid.CellValidating += Grid_CellValidating;
DataGridViewTextBoxColumn nameColumn = new DataGridViewTextBoxColumn();
nameColumn.DataPropertyName = "Name";
nameColumn.HeaderText = "업종";
nameColumn.ReadOnly = true;
nameColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
nameColumn.FillWeight = 130f;
DataGridViewTextBoxColumn capColumn = new DataGridViewTextBoxColumn();
capColumn.DataPropertyName = "MarketCap";
capColumn.HeaderText = "시가총액";
capColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
capColumn.FillWeight = 90f;
capColumn.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
capColumn.DefaultCellStyle.Format = "N0";
DataGridViewTextBoxColumn rateColumn = new DataGridViewTextBoxColumn();
rateColumn.DataPropertyName = "ChangeRate";
rateColumn.HeaderText = "등락률(%)";
rateColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
rateColumn.FillWeight = 90f;
rateColumn.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
rateColumn.DefaultCellStyle.Format = "+0.00;-0.00;0.00";
_grid.Columns.Add(nameColumn);
_grid.Columns.Add(capColumn);
_grid.Columns.Add(rateColumn);
_grid.DataSource = _items;
_statusLabel = new Label();
_statusLabel.Dock = DockStyle.Bottom;
_statusLabel.Height = 30;
_statusLabel.TextAlign = ContentAlignment.MiddleLeft;
_statusLabel.ForeColor = Color.FromArgb(80, 86, 96);
leftPanel.Controls.Add(_grid);
leftPanel.Controls.Add(toolbar);
leftPanel.Controls.Add(_statusLabel);
Panel previewPanel = new Panel();
previewPanel.Dock = DockStyle.Fill;
previewPanel.Padding = new Padding(14);
previewPanel.BackColor = Color.FromArgb(29, 32, 39);
_previewBox = new PictureBox();
_previewBox.Dock = DockStyle.Fill;
_previewBox.BackColor = Color.FromArgb(20, 22, 28);
_previewBox.SizeMode = PictureBoxSizeMode.Zoom;
previewPanel.Controls.Add(_previewBox);
splitContainer.Panel1.Controls.Add(leftPanel);
splitContainer.Panel2.Controls.Add(previewPanel);
Controls.Add(splitContainer);
}
private void LoadDefaultItems()
{
_loading = true;
_items.Clear();
string[] sectors =
{
"전기전자",
"금융업",
"서비스업",
"운수장비",
"화학",
"통신업",
"의약품",
"철강금속",
"유통업",
"건설업",
"음식료품",
"기계",
"운수창고",
"전기가스업",
"보험",
"증권"
};
foreach (string sector in sectors)
{
_items.Add(new SectorHeatMapItem
{
Name = sector,
MarketCap = 100m,
ChangeRate = 0m
});
}
_loading = false;
}
private void RandomizeValues()
{
_loading = true;
foreach (SectorHeatMapItem item in _items)
{
item.MarketCap = _random.Next(25, 260);
item.ChangeRate = Math.Round((decimal)(_random.NextDouble() * 60d - 30d), 2);
}
_loading = false;
_grid.Refresh();
RefreshPreview();
}
private void RefreshPreview()
{
if (_loading || _previewBox == null)
{
return;
}
List<SectorHeatMapItem> snapshot = _items
.Select(item => new SectorHeatMapItem
{
Name = item.Name,
MarketCap = item.MarketCap,
ChangeRate = item.ChangeRate
})
.ToList();
Bitmap newImage = HeatMapRenderer.Render(snapshot);
Image oldImage = _previewBox.Image;
_previewBox.Image = newImage;
if (oldImage != null)
{
oldImage.Dispose();
}
_statusLabel.Text = string.Format(
CultureInfo.CurrentCulture,
"출력 {0:N0} x {1:N0}px / 색상 기준 ±{2:N0}%",
HeatMapRenderer.ExportWidth,
HeatMapRenderer.ExportHeight,
HeatMapRenderer.MaxRatePercent);
}
private void RandomButton_Click(object sender, EventArgs e)
{
RandomizeValues();
}
private void SaveButton_Click(object sender, EventArgs e)
{
if (!_grid.EndEdit())
{
_statusLabel.Text = "입력값을 확인해 주세요.";
return;
}
using (SaveFileDialog dialog = new SaveFileDialog())
{
dialog.Filter = "PNG 이미지 (*.png)|*.png";
dialog.FileName = "sector_heatmap_" + DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture) + ".png";
dialog.Title = "업종별 증시 이미지 저장";
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
using (Bitmap bitmap = HeatMapRenderer.Render(_items))
{
bitmap.Save(dialog.FileName, ImageFormat.Png);
}
_statusLabel.Text = "저장 완료: " + dialog.FileName;
}
}
private void Grid_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
RefreshPreview();
}
private void Grid_CellParsing(object sender, DataGridViewCellParsingEventArgs e)
{
if (_grid.Columns[e.ColumnIndex].DataPropertyName == "Name")
{
return;
}
decimal value;
string text = Convert.ToString(e.Value, CultureInfo.CurrentCulture);
if (TryParseDecimal(text, out value))
{
e.Value = value;
e.ParsingApplied = true;
}
}
private void Grid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
if (_grid.Columns[e.ColumnIndex].DataPropertyName == "Name")
{
return;
}
decimal value;
string text = Convert.ToString(e.FormattedValue, CultureInfo.CurrentCulture);
if (!TryParseDecimal(text, out value))
{
e.Cancel = true;
_grid.Rows[e.RowIndex].ErrorText = "숫자를 입력하세요.";
return;
}
if (_grid.Columns[e.ColumnIndex].DataPropertyName == "MarketCap" && value <= 0m)
{
e.Cancel = true;
_grid.Rows[e.RowIndex].ErrorText = "시가총액은 0보다 커야 합니다.";
return;
}
_grid.Rows[e.RowIndex].ErrorText = string.Empty;
}
private void Grid_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
e.ThrowException = false;
_statusLabel.Text = "입력값을 확인해 주세요.";
}
private static bool TryParseDecimal(string text, out decimal value)
{
value = 0m;
if (text == null)
{
return false;
}
string normalized = text.Trim().Replace("%", string.Empty);
return decimal.TryParse(normalized, NumberStyles.Number, CultureInfo.CurrentCulture, out value)
|| decimal.TryParse(normalized, NumberStyles.Number, CultureInfo.InvariantCulture, out value);
}
}
}