From 74306da88bc4c1c96121bb5aafe181756dd015e8 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 30 Jun 2026 23:41:41 +0900 Subject: [PATCH] Add configurable heatmap color ranges --- mbn_stock/ColorRangeRule.cs | 141 +++++++++++++++++++++++ mbn_stock/Form1.Designer.cs | 216 ++++++++++++++++++++++++++++++----- mbn_stock/Form1.cs | 163 +++++++++++++++++++++++++- mbn_stock/HeatMapRenderer.cs | 118 ++++++++++++++++--- mbn_stock/mbn_stock.csproj | 1 + 5 files changed, 590 insertions(+), 49 deletions(-) create mode 100644 mbn_stock/ColorRangeRule.cs diff --git a/mbn_stock/ColorRangeRule.cs b/mbn_stock/ColorRangeRule.cs new file mode 100644 index 0000000..970db1a --- /dev/null +++ b/mbn_stock/ColorRangeRule.cs @@ -0,0 +1,141 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Globalization; + +namespace mbn_stock +{ + public class ColorRangeRule : INotifyPropertyChanged + { + private decimal _fromRate; + private decimal _toRate; + private string _colorHex = "#808080"; + + public event PropertyChangedEventHandler PropertyChanged; + + public decimal FromRate + { + get { return _fromRate; } + set + { + if (_fromRate == value) + { + return; + } + + _fromRate = value; + OnPropertyChanged("FromRate"); + } + } + + public decimal ToRate + { + get { return _toRate; } + set + { + if (_toRate == value) + { + return; + } + + _toRate = value; + OnPropertyChanged("ToRate"); + } + } + + public string ColorHex + { + get { return _colorHex; } + set + { + string normalized = NormalizeColorHex(value); + if (_colorHex == normalized) + { + return; + } + + _colorHex = normalized; + OnPropertyChanged("ColorHex"); + } + } + + public decimal MinRate + { + get { return Math.Min(FromRate, ToRate); } + } + + public decimal MaxRate + { + get { return Math.Max(FromRate, ToRate); } + } + + public Color DrawingColor + { + get + { + Color color; + return TryParseColor(ColorHex, out color) ? color : Color.Gray; + } + } + + public bool Contains(decimal value) + { + return value >= MinRate && value < MaxRate; + } + + public static bool TryParseColor(string text, out Color color) + { + color = Color.Empty; + + try + { + string normalized = NormalizeColorHex(text); + if (string.IsNullOrEmpty(normalized)) + { + return false; + } + + color = ColorTranslator.FromHtml(normalized); + return true; + } + catch (Exception) + { + return false; + } + } + + public static string NormalizeColorHex(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return "#808080"; + } + + string normalized = text.Trim(); + if (!normalized.StartsWith("#", StringComparison.Ordinal)) + { + normalized = "#" + normalized; + } + + if (normalized.Length == 7) + { + int value; + if (int.TryParse(normalized.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value)) + { + return normalized.ToUpperInvariant(); + } + } + + return normalized; + } + + private void OnPropertyChanged(string propertyName) + { + PropertyChangedEventHandler handler = PropertyChanged; + if (handler != null) + { + handler(this, new PropertyChangedEventArgs(propertyName)); + } + } + } +} diff --git a/mbn_stock/Form1.Designer.cs b/mbn_stock/Form1.Designer.cs index 2c8db78..e4934ae 100644 --- a/mbn_stock/Form1.Designer.cs +++ b/mbn_stock/Form1.Designer.cs @@ -32,12 +32,25 @@ namespace mbn_stock System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle(); System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle3 = new System.Windows.Forms.DataGridViewCellStyle(); System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle4 = new System.Windows.Forms.DataGridViewCellStyle(); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle5 = new System.Windows.Forms.DataGridViewCellStyle(); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle6 = new System.Windows.Forms.DataGridViewCellStyle(); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle7 = new System.Windows.Forms.DataGridViewCellStyle(); + System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle8 = new System.Windows.Forms.DataGridViewCellStyle(); this._splitContainer = new System.Windows.Forms.SplitContainer(); this._leftPanel = new System.Windows.Forms.Panel(); this._grid = new System.Windows.Forms.DataGridView(); this._nameColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); this._capColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); this._rateColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this._colorRulePanel = new System.Windows.Forms.Panel(); + this._colorRuleGrid = new System.Windows.Forms.DataGridView(); + this._fromRateColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this._toRateColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this._colorHexColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this._colorPreviewColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this._colorRuleHeaderPanel = new System.Windows.Forms.Panel(); + this._colorRuleTitleLabel = new System.Windows.Forms.Label(); + this._resetColorRulesButton = new System.Windows.Forms.Button(); this._toolbar = new System.Windows.Forms.FlowLayoutPanel(); this._randomButton = new System.Windows.Forms.Button(); this._saveButton = new System.Windows.Forms.Button(); @@ -50,36 +63,40 @@ namespace mbn_stock this._splitContainer.SuspendLayout(); this._leftPanel.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this._grid)).BeginInit(); + this._colorRulePanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this._colorRuleGrid)).BeginInit(); + this._colorRuleHeaderPanel.SuspendLayout(); this._toolbar.SuspendLayout(); this._previewPanel.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this._previewBox)).BeginInit(); this.SuspendLayout(); - // + // // _splitContainer - // + // this._splitContainer.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(210)))), ((int)(((byte)(214)))), ((int)(((byte)(220))))); this._splitContainer.Dock = System.Windows.Forms.DockStyle.Fill; this._splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1; this._splitContainer.Location = new System.Drawing.Point(0, 0); this._splitContainer.Name = "_splitContainer"; - // + // // _splitContainer.Panel1 - // + // this._splitContainer.Panel1.Controls.Add(this._leftPanel); this._splitContainer.Panel1MinSize = 420; - // + // // _splitContainer.Panel2 - // + // this._splitContainer.Panel2.Controls.Add(this._previewPanel); this._splitContainer.Panel2MinSize = 520; this._splitContainer.Size = new System.Drawing.Size(1280, 760); this._splitContainer.SplitterDistance = 500; this._splitContainer.TabIndex = 0; - // + // // _leftPanel - // + // this._leftPanel.BackColor = System.Drawing.Color.White; this._leftPanel.Controls.Add(this._grid); + this._leftPanel.Controls.Add(this._colorRulePanel); this._leftPanel.Controls.Add(this._toolbar); this._leftPanel.Controls.Add(this._statusLabel); this._leftPanel.Dock = System.Windows.Forms.DockStyle.Fill; @@ -88,9 +105,9 @@ namespace mbn_stock this._leftPanel.Padding = new System.Windows.Forms.Padding(14); this._leftPanel.Size = new System.Drawing.Size(500, 760); this._leftPanel.TabIndex = 0; - // + // // _grid - // + // this._grid.AllowUserToAddRows = false; this._grid.AllowUserToDeleteRows = false; this._grid.AllowUserToResizeRows = false; @@ -122,24 +139,24 @@ namespace mbn_stock this._grid.RowHeadersVisible = false; this._grid.RowTemplate.Height = 23; this._grid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect; - this._grid.Size = new System.Drawing.Size(472, 656); + this._grid.Size = new System.Drawing.Size(472, 446); this._grid.TabIndex = 1; this._grid.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.Grid_CellEndEdit); this._grid.CellParsing += new System.Windows.Forms.DataGridViewCellParsingEventHandler(this.Grid_CellParsing); this._grid.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.Grid_CellValidating); this._grid.DataError += new System.Windows.Forms.DataGridViewDataErrorEventHandler(this.Grid_DataError); - // + // // _nameColumn - // + // this._nameColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; this._nameColumn.DataPropertyName = "Name"; this._nameColumn.FillWeight = 130F; this._nameColumn.HeaderText = "업종"; this._nameColumn.Name = "_nameColumn"; this._nameColumn.ReadOnly = true; - // + // // _capColumn - // + // this._capColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; this._capColumn.DataPropertyName = "MarketCap"; dataGridViewCellStyle3.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight; @@ -148,9 +165,9 @@ namespace mbn_stock this._capColumn.FillWeight = 90F; this._capColumn.HeaderText = "시가총액"; this._capColumn.Name = "_capColumn"; - // + // // _rateColumn - // + // this._rateColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; this._rateColumn.DataPropertyName = "ChangeRate"; dataGridViewCellStyle4.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight; @@ -159,9 +176,134 @@ namespace mbn_stock this._rateColumn.FillWeight = 90F; this._rateColumn.HeaderText = "등락률(%)"; this._rateColumn.Name = "_rateColumn"; - // + // + // _colorRulePanel + // + this._colorRulePanel.Controls.Add(this._colorRuleGrid); + this._colorRulePanel.Controls.Add(this._colorRuleHeaderPanel); + this._colorRulePanel.Dock = System.Windows.Forms.DockStyle.Bottom; + this._colorRulePanel.Location = new System.Drawing.Point(14, 506); + this._colorRulePanel.Name = "_colorRulePanel"; + this._colorRulePanel.Padding = new System.Windows.Forms.Padding(0, 8, 0, 0); + this._colorRulePanel.Size = new System.Drawing.Size(472, 210); + this._colorRulePanel.TabIndex = 2; + // + // _colorRuleGrid + // + this._colorRuleGrid.AllowUserToResizeRows = false; + this._colorRuleGrid.AutoGenerateColumns = false; + this._colorRuleGrid.BackgroundColor = System.Drawing.Color.White; + this._colorRuleGrid.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + dataGridViewCellStyle5.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(238)))), ((int)(((byte)(241)))), ((int)(((byte)(245))))); + dataGridViewCellStyle5.Font = new System.Drawing.Font("Malgun Gothic", 9.5F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129))); + dataGridViewCellStyle5.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(35)))), ((int)(((byte)(45))))); + this._colorRuleGrid.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle5; + this._colorRuleGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this._colorRuleGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this._fromRateColumn, + this._toRateColumn, + this._colorHexColumn, + this._colorPreviewColumn}); + dataGridViewCellStyle6.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; + dataGridViewCellStyle6.BackColor = System.Drawing.SystemColors.Window; + dataGridViewCellStyle6.Font = new System.Drawing.Font("Malgun Gothic", 9.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129))); + dataGridViewCellStyle6.ForeColor = System.Drawing.SystemColors.ControlText; + dataGridViewCellStyle6.SelectionBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(218)))), ((int)(((byte)(232)))), ((int)(((byte)(255))))); + dataGridViewCellStyle6.SelectionForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(24)))), ((int)(((byte)(32))))); + dataGridViewCellStyle6.WrapMode = System.Windows.Forms.DataGridViewTriState.False; + this._colorRuleGrid.DefaultCellStyle = dataGridViewCellStyle6; + this._colorRuleGrid.Dock = System.Windows.Forms.DockStyle.Fill; + this._colorRuleGrid.EnableHeadersVisualStyles = false; + this._colorRuleGrid.Location = new System.Drawing.Point(0, 40); + this._colorRuleGrid.MultiSelect = false; + this._colorRuleGrid.Name = "_colorRuleGrid"; + this._colorRuleGrid.RowHeadersVisible = false; + this._colorRuleGrid.RowTemplate.Height = 23; + this._colorRuleGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect; + this._colorRuleGrid.Size = new System.Drawing.Size(472, 170); + this._colorRuleGrid.TabIndex = 1; + this._colorRuleGrid.CellDoubleClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.ColorRuleGrid_CellDoubleClick); + this._colorRuleGrid.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.ColorRuleGrid_CellEndEdit); + this._colorRuleGrid.CellFormatting += new System.Windows.Forms.DataGridViewCellFormattingEventHandler(this.ColorRuleGrid_CellFormatting); + this._colorRuleGrid.CellParsing += new System.Windows.Forms.DataGridViewCellParsingEventHandler(this.ColorRuleGrid_CellParsing); + this._colorRuleGrid.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.ColorRuleGrid_CellValidating); + this._colorRuleGrid.DataError += new System.Windows.Forms.DataGridViewDataErrorEventHandler(this.ColorRuleGrid_DataError); + this._colorRuleGrid.DefaultValuesNeeded += new System.Windows.Forms.DataGridViewRowEventHandler(this.ColorRuleGrid_DefaultValuesNeeded); + this._colorRuleGrid.RowsRemoved += new System.Windows.Forms.DataGridViewRowsRemovedEventHandler(this.ColorRuleGrid_RowsRemoved); + // + // _fromRateColumn + // + this._fromRateColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this._fromRateColumn.DataPropertyName = "FromRate"; + dataGridViewCellStyle7.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight; + dataGridViewCellStyle7.Format = "0.##"; + this._fromRateColumn.DefaultCellStyle = dataGridViewCellStyle7; + this._fromRateColumn.FillWeight = 60F; + this._fromRateColumn.HeaderText = "시작%"; + this._fromRateColumn.Name = "_fromRateColumn"; + // + // _toRateColumn + // + this._toRateColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this._toRateColumn.DataPropertyName = "ToRate"; + dataGridViewCellStyle8.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight; + dataGridViewCellStyle8.Format = "0.##"; + this._toRateColumn.DefaultCellStyle = dataGridViewCellStyle8; + this._toRateColumn.FillWeight = 60F; + this._toRateColumn.HeaderText = "종료%"; + this._toRateColumn.Name = "_toRateColumn"; + // + // _colorHexColumn + // + this._colorHexColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this._colorHexColumn.DataPropertyName = "ColorHex"; + this._colorHexColumn.FillWeight = 82F; + this._colorHexColumn.HeaderText = "색상"; + this._colorHexColumn.Name = "_colorHexColumn"; + // + // _colorPreviewColumn + // + this._colorPreviewColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this._colorPreviewColumn.FillWeight = 38F; + this._colorPreviewColumn.HeaderText = "보기"; + this._colorPreviewColumn.Name = "_colorPreviewColumn"; + this._colorPreviewColumn.ReadOnly = true; + // + // _colorRuleHeaderPanel + // + this._colorRuleHeaderPanel.Controls.Add(this._colorRuleTitleLabel); + this._colorRuleHeaderPanel.Controls.Add(this._resetColorRulesButton); + this._colorRuleHeaderPanel.Dock = System.Windows.Forms.DockStyle.Top; + this._colorRuleHeaderPanel.Location = new System.Drawing.Point(0, 8); + this._colorRuleHeaderPanel.Name = "_colorRuleHeaderPanel"; + this._colorRuleHeaderPanel.Size = new System.Drawing.Size(472, 32); + this._colorRuleHeaderPanel.TabIndex = 0; + // + // _colorRuleTitleLabel + // + this._colorRuleTitleLabel.Dock = System.Windows.Forms.DockStyle.Fill; + this._colorRuleTitleLabel.Font = new System.Drawing.Font("Malgun Gothic", 9.5F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129))); + this._colorRuleTitleLabel.Location = new System.Drawing.Point(0, 0); + this._colorRuleTitleLabel.Name = "_colorRuleTitleLabel"; + this._colorRuleTitleLabel.Size = new System.Drawing.Size(362, 32); + this._colorRuleTitleLabel.TabIndex = 0; + this._colorRuleTitleLabel.Text = "등락률 색상 규칙"; + this._colorRuleTitleLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // _resetColorRulesButton + // + this._resetColorRulesButton.Dock = System.Windows.Forms.DockStyle.Right; + this._resetColorRulesButton.Location = new System.Drawing.Point(362, 0); + this._resetColorRulesButton.Name = "_resetColorRulesButton"; + this._resetColorRulesButton.Size = new System.Drawing.Size(110, 32); + this._resetColorRulesButton.TabIndex = 1; + this._resetColorRulesButton.Text = "기본 색상"; + this._resetColorRulesButton.UseVisualStyleBackColor = true; + this._resetColorRulesButton.Click += new System.EventHandler(this.ResetColorRulesButton_Click); + // + // // _toolbar - // + // this._toolbar.Controls.Add(this._randomButton); this._toolbar.Controls.Add(this._saveButton); this._toolbar.Dock = System.Windows.Forms.DockStyle.Top; @@ -171,9 +313,9 @@ namespace mbn_stock this._toolbar.Size = new System.Drawing.Size(472, 46); this._toolbar.TabIndex = 0; this._toolbar.WrapContents = false; - // + // // _randomButton - // + // this._randomButton.Location = new System.Drawing.Point(3, 3); this._randomButton.Name = "_randomButton"; this._randomButton.Size = new System.Drawing.Size(110, 32); @@ -181,9 +323,9 @@ namespace mbn_stock this._randomButton.Text = "랜덤 값"; this._randomButton.UseVisualStyleBackColor = true; this._randomButton.Click += new System.EventHandler(this.RandomButton_Click); - // + // // _saveButton - // + // this._saveButton.Location = new System.Drawing.Point(119, 3); this._saveButton.Name = "_saveButton"; this._saveButton.Size = new System.Drawing.Size(110, 32); @@ -191,9 +333,9 @@ namespace mbn_stock this._saveButton.Text = "PNG 저장"; this._saveButton.UseVisualStyleBackColor = true; this._saveButton.Click += new System.EventHandler(this.SaveButton_Click); - // + // // _statusLabel - // + // this._statusLabel.Dock = System.Windows.Forms.DockStyle.Bottom; this._statusLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(80)))), ((int)(((byte)(86)))), ((int)(((byte)(96))))); this._statusLabel.Location = new System.Drawing.Point(14, 716); @@ -201,9 +343,9 @@ namespace mbn_stock this._statusLabel.Size = new System.Drawing.Size(472, 30); this._statusLabel.TabIndex = 2; this._statusLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; - // + // // _previewPanel - // + // this._previewPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(29)))), ((int)(((byte)(32)))), ((int)(((byte)(39))))); this._previewPanel.Controls.Add(this._previewBox); this._previewPanel.Dock = System.Windows.Forms.DockStyle.Fill; @@ -212,9 +354,9 @@ namespace mbn_stock this._previewPanel.Padding = new System.Windows.Forms.Padding(14); this._previewPanel.Size = new System.Drawing.Size(776, 760); this._previewPanel.TabIndex = 0; - // + // // _previewBox - // + // this._previewBox.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(22)))), ((int)(((byte)(28))))); this._previewBox.Dock = System.Windows.Forms.DockStyle.Fill; this._previewBox.Location = new System.Drawing.Point(14, 14); @@ -223,9 +365,9 @@ namespace mbn_stock this._previewBox.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; this._previewBox.TabIndex = 0; this._previewBox.TabStop = false; - // + // // Form1 - // + // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 17F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(245)))), ((int)(((byte)(246)))), ((int)(((byte)(248))))); @@ -241,6 +383,9 @@ namespace mbn_stock this._splitContainer.ResumeLayout(false); this._leftPanel.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this._grid)).EndInit(); + this._colorRulePanel.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this._colorRuleGrid)).EndInit(); + this._colorRuleHeaderPanel.ResumeLayout(false); this._toolbar.ResumeLayout(false); this._previewPanel.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this._previewBox)).EndInit(); @@ -256,6 +401,15 @@ namespace mbn_stock private System.Windows.Forms.DataGridViewTextBoxColumn _nameColumn; private System.Windows.Forms.DataGridViewTextBoxColumn _capColumn; private System.Windows.Forms.DataGridViewTextBoxColumn _rateColumn; + private System.Windows.Forms.Panel _colorRulePanel; + private System.Windows.Forms.DataGridView _colorRuleGrid; + private System.Windows.Forms.DataGridViewTextBoxColumn _fromRateColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn _toRateColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn _colorHexColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn _colorPreviewColumn; + private System.Windows.Forms.Panel _colorRuleHeaderPanel; + private System.Windows.Forms.Label _colorRuleTitleLabel; + private System.Windows.Forms.Button _resetColorRulesButton; private System.Windows.Forms.FlowLayoutPanel _toolbar; private System.Windows.Forms.Button _randomButton; private System.Windows.Forms.Button _saveButton; diff --git a/mbn_stock/Form1.cs b/mbn_stock/Form1.cs index 26b6bb0..fab9d5f 100644 --- a/mbn_stock/Form1.cs +++ b/mbn_stock/Form1.cs @@ -12,6 +12,7 @@ namespace mbn_stock public partial class Form1 : Form { private readonly BindingList _items = new BindingList(); + private readonly BindingList _colorRules = new BindingList(); private readonly Random _random = new Random(); private bool _loading; @@ -25,7 +26,9 @@ namespace mbn_stock } _grid.DataSource = _items; + _colorRuleGrid.DataSource = _colorRules; LoadDefaultItems(); + LoadDefaultColorRules(); RandomizeValues(); } @@ -78,6 +81,20 @@ namespace mbn_stock _loading = false; } + private void LoadDefaultColorRules() + { + _loading = true; + _colorRules.Clear(); + + foreach (ColorRangeRule rule in HeatMapRenderer.CreateDefaultColorRules()) + { + _colorRules.Add(rule); + } + + _loading = false; + _colorRuleGrid.Refresh(); + } + private void RandomizeValues() { _loading = true; @@ -109,7 +126,7 @@ namespace mbn_stock }) .ToList(); - Bitmap newImage = HeatMapRenderer.Render(snapshot); + Bitmap newImage = HeatMapRenderer.Render(snapshot, _colorRules); Image oldImage = _previewBox.Image; _previewBox.Image = newImage; @@ -120,10 +137,10 @@ namespace mbn_stock _statusLabel.Text = string.Format( CultureInfo.CurrentCulture, - "출력 {0:N0} x {1:N0}px / 색상 기준 ±{2:N0}%", + "출력 {0:N0} x {1:N0}px / 색상 규칙 {2:N0}개", HeatMapRenderer.ExportWidth, HeatMapRenderer.ExportHeight, - HeatMapRenderer.MaxRatePercent); + _colorRules.Count); } private void RandomButton_Click(object sender, EventArgs e) @@ -150,15 +167,21 @@ namespace mbn_stock return; } - using (Bitmap bitmap = HeatMapRenderer.Render(_items)) + using (Bitmap bitmap = HeatMapRenderer.Render(_items, _colorRules)) { bitmap.Save(dialog.FileName, ImageFormat.Png); } - _statusLabel.Text = "저장 완료: " + dialog.FileName; + _statusLabel.Text = "저장 완료: " + dialog.FileName; } } + private void ResetColorRulesButton_Click(object sender, EventArgs e) + { + LoadDefaultColorRules(); + RefreshPreview(); + } + private void Grid_CellEndEdit(object sender, DataGridViewCellEventArgs e) { RefreshPreview(); @@ -214,6 +237,131 @@ namespace mbn_stock _statusLabel.Text = "입력값을 확인해 주세요."; } + private void ColorRuleGrid_CellDoubleClick(object sender, DataGridViewCellEventArgs e) + { + if (e.RowIndex < 0 || + (_colorRuleGrid.Columns[e.ColumnIndex] != _colorHexColumn && + _colorRuleGrid.Columns[e.ColumnIndex] != _colorPreviewColumn)) + { + return; + } + + ColorRangeRule rule = _colorRuleGrid.Rows[e.RowIndex].DataBoundItem as ColorRangeRule; + if (rule == null) + { + return; + } + + using (ColorDialog dialog = new ColorDialog()) + { + dialog.FullOpen = true; + dialog.Color = rule.DrawingColor; + + if (dialog.ShowDialog(this) != DialogResult.OK) + { + return; + } + + rule.ColorHex = ToHex(dialog.Color); + _colorRuleGrid.Refresh(); + RefreshPreview(); + } + } + + private void ColorRuleGrid_CellEndEdit(object sender, DataGridViewCellEventArgs e) + { + _colorRuleGrid.Invalidate(); + RefreshPreview(); + } + + private void ColorRuleGrid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) + { + if (e.RowIndex < 0 || _colorRuleGrid.Columns[e.ColumnIndex] != _colorPreviewColumn) + { + return; + } + + ColorRangeRule rule = _colorRuleGrid.Rows[e.RowIndex].DataBoundItem as ColorRangeRule; + if (rule == null) + { + return; + } + + e.Value = string.Empty; + e.CellStyle.BackColor = rule.DrawingColor; + e.CellStyle.SelectionBackColor = rule.DrawingColor; + e.FormattingApplied = true; + } + + private void ColorRuleGrid_CellParsing(object sender, DataGridViewCellParsingEventArgs e) + { + string propertyName = _colorRuleGrid.Columns[e.ColumnIndex].DataPropertyName; + + if (propertyName == "FromRate" || propertyName == "ToRate") + { + decimal value; + string text = Convert.ToString(e.Value, CultureInfo.CurrentCulture); + + if (TryParseDecimal(text, out value)) + { + e.Value = value; + e.ParsingApplied = true; + } + } + else if (propertyName == "ColorHex") + { + e.Value = ColorRangeRule.NormalizeColorHex(Convert.ToString(e.Value, CultureInfo.CurrentCulture)); + e.ParsingApplied = true; + } + } + + private void ColorRuleGrid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e) + { + string propertyName = _colorRuleGrid.Columns[e.ColumnIndex].DataPropertyName; + string text = Convert.ToString(e.FormattedValue, CultureInfo.CurrentCulture); + + if (propertyName == "FromRate" || propertyName == "ToRate") + { + decimal value; + if (!TryParseDecimal(text, out value)) + { + e.Cancel = true; + _colorRuleGrid.Rows[e.RowIndex].ErrorText = "숫자를 입력하세요."; + return; + } + } + else if (propertyName == "ColorHex") + { + Color color; + if (!ColorRangeRule.TryParseColor(text, out color)) + { + e.Cancel = true; + _colorRuleGrid.Rows[e.RowIndex].ErrorText = "#RRGGBB 형식으로 입력하세요."; + return; + } + } + + _colorRuleGrid.Rows[e.RowIndex].ErrorText = string.Empty; + } + + private void ColorRuleGrid_DataError(object sender, DataGridViewDataErrorEventArgs e) + { + e.ThrowException = false; + _statusLabel.Text = "색상 규칙 입력값을 확인해 주세요."; + } + + private void ColorRuleGrid_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e) + { + e.Row.Cells[_fromRateColumn.Index].Value = 0m; + e.Row.Cells[_toRateColumn.Index].Value = 0m; + e.Row.Cells[_colorHexColumn.Index].Value = "#808080"; + } + + private void ColorRuleGrid_RowsRemoved(object sender, DataGridViewRowsRemovedEventArgs e) + { + RefreshPreview(); + } + private static bool TryParseDecimal(string text, out decimal value) { value = 0m; @@ -227,5 +375,10 @@ namespace mbn_stock return decimal.TryParse(normalized, NumberStyles.Number, CultureInfo.CurrentCulture, out value) || decimal.TryParse(normalized, NumberStyles.Number, CultureInfo.InvariantCulture, out value); } + + private static string ToHex(Color color) + { + return string.Format(CultureInfo.InvariantCulture, "#{0:X2}{1:X2}{2:X2}", color.R, color.G, color.B); + } } } diff --git a/mbn_stock/HeatMapRenderer.cs b/mbn_stock/HeatMapRenderer.cs index 708c3a1..a1c8b24 100644 --- a/mbn_stock/HeatMapRenderer.cs +++ b/mbn_stock/HeatMapRenderer.cs @@ -20,10 +20,20 @@ namespace mbn_stock public static Bitmap Render(IEnumerable sourceItems) { - return Render(sourceItems, new Size(ExportWidth, ExportHeight)); + return Render(sourceItems, new Size(ExportWidth, ExportHeight), CreateDefaultColorRules()); + } + + public static Bitmap Render(IEnumerable sourceItems, IEnumerable colorRules) + { + return Render(sourceItems, new Size(ExportWidth, ExportHeight), colorRules); } public static Bitmap Render(IEnumerable sourceItems, Size size) + { + return Render(sourceItems, size, CreateDefaultColorRules()); + } + + public static Bitmap Render(IEnumerable sourceItems, Size size, IEnumerable colorRules) { Bitmap bitmap = new Bitmap(size.Width, size.Height, PixelFormat.Format32bppArgb); @@ -39,6 +49,7 @@ namespace mbn_stock .Where(item => item != null && item.MarketCap > 0m) .OrderByDescending(item => item.MarketCap) .ToList(); + List rules = NormalizeColorRules(colorRules); if (items.Count == 0) { @@ -48,12 +59,35 @@ namespace mbn_stock RectangleF canvas = new RectangleF(0, 0, size.Width, size.Height); List nodes = BuildNodes(items, canvas); - DrawTiles(graphics, nodes); + DrawTiles(graphics, nodes, rules); } return bitmap; } + public static List CreateDefaultColorRules() + { + return new List + { + CreateColorRule(-30m, -20m, "#0046BE"), + CreateColorRule(-20m, -10m, "#1F66C8"), + CreateColorRule(-10m, 0m, "#5F8FD8"), + CreateColorRule(0m, 10m, "#D85F5F"), + CreateColorRule(10m, 20m, "#D82C2C"), + CreateColorRule(20m, 30m, "#B60000") + }; + } + + private static ColorRangeRule CreateColorRule(decimal fromRate, decimal toRate, string colorHex) + { + return new ColorRangeRule + { + FromRate = fromRate, + ToRate = toRate, + ColorHex = colorHex + }; + } + private static List BuildNodes(IList items, RectangleF bounds) { decimal total = items.Sum(item => item.MarketCap); @@ -133,7 +167,7 @@ namespace mbn_stock return splitIndex; } - private static void DrawTiles(Graphics graphics, IList nodes) + private static void DrawTiles(Graphics graphics, IList nodes, IList colorRules) { foreach (TreeMapNode node in nodes) { @@ -143,7 +177,7 @@ namespace mbn_stock continue; } - Color fill = GetFillColor(node.Item.ChangeRate); + Color fill = GetFillColor(node.Item.ChangeRate, colorRules); using (Brush brush = new SolidBrush(fill)) using (Pen borderPen = new Pen(Color.FromArgb(70, 255, 255, 255), 1.5f)) { @@ -174,14 +208,13 @@ namespace mbn_stock textBounds = Inset(tileBounds, 2f); } - Color textColor = GetReadableTextColor(fill); string rateText = FormatRate(item.ChangeRate); float fontSize = FindFittingFontSize(graphics, item.Name, rateText, textBounds); float rateFontSize = Math.Max(8f, fontSize * 0.78f); using (Font nameFont = new Font("Malgun Gothic", fontSize, FontStyle.Bold, GraphicsUnit.Pixel)) using (Font rateFont = new Font("Malgun Gothic", rateFontSize, FontStyle.Bold, GraphicsUnit.Pixel)) - using (Brush textBrush = new SolidBrush(textColor)) + using (Brush textBrush = new SolidBrush(Color.White)) using (StringFormat centerFormat = new StringFormat()) { centerFormat.Alignment = StringAlignment.Center; @@ -227,7 +260,53 @@ namespace mbn_stock return 8f; } - private static Color GetFillColor(decimal changeRate) + private static Color GetFillColor(decimal changeRate, IList colorRules) + { + if (colorRules != null && colorRules.Count > 0) + { + Color ruleColor; + if (TryGetRuleColor(changeRate, colorRules, out ruleColor)) + { + return ruleColor; + } + } + + return GetDefaultFillColor(changeRate); + } + + private static bool TryGetRuleColor(decimal changeRate, IList colorRules, out Color color) + { + color = Color.Empty; + + for (int index = 0; index < colorRules.Count; index++) + { + ColorRangeRule rule = colorRules[index]; + if (rule.Contains(changeRate)) + { + color = rule.DrawingColor; + return true; + } + } + + ColorRangeRule firstRule = colorRules.First(); + ColorRangeRule lastRule = colorRules.Last(); + + if (changeRate < firstRule.MinRate) + { + color = firstRule.DrawingColor; + return true; + } + + if (changeRate >= lastRule.MaxRate) + { + color = lastRule.DrawingColor; + return true; + } + + return false; + } + + private static Color GetDefaultFillColor(decimal changeRate) { double intensity = Math.Min(1d, Math.Abs(decimal.ToDouble(changeRate)) / decimal.ToDouble(MaxRatePercent)); @@ -244,6 +323,25 @@ namespace mbn_stock return Color.FromArgb(226, 228, 232); } + private static List NormalizeColorRules(IEnumerable colorRules) + { + List rules = colorRules == null + ? new List() + : colorRules + .Where(rule => rule != null && rule.MinRate != rule.MaxRate && IsValidColorRule(rule)) + .OrderBy(rule => rule.MinRate) + .ThenBy(rule => rule.MaxRate) + .ToList(); + + return rules.Count == 0 ? CreateDefaultColorRules() : rules; + } + + private static bool IsValidColorRule(ColorRangeRule rule) + { + Color color; + return ColorRangeRule.TryParseColor(rule.ColorHex, out color); + } + private static Color Interpolate(Color from, Color to, double amount) { amount = Math.Max(0d, Math.Min(1d, amount)); @@ -253,12 +351,6 @@ namespace mbn_stock return Color.FromArgb(r, g, b); } - private static Color GetReadableTextColor(Color background) - { - double luminance = (0.299d * background.R) + (0.587d * background.G) + (0.114d * background.B); - return luminance < 145d ? Color.White : Color.FromArgb(24, 28, 36); - } - private static string FormatRate(decimal value) { string format = value > 0m ? "+0.00'%'" diff --git a/mbn_stock/mbn_stock.csproj b/mbn_stock/mbn_stock.csproj index 5253a4d..ed8107b 100644 --- a/mbn_stock/mbn_stock.csproj +++ b/mbn_stock/mbn_stock.csproj @@ -46,6 +46,7 @@ + Form