Add configurable heatmap color ranges
This commit is contained in:
141
mbn_stock/ColorRangeRule.cs
Normal file
141
mbn_stock/ColorRangeRule.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
216
mbn_stock/Form1.Designer.cs
generated
216
mbn_stock/Form1.Designer.cs
generated
@@ -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;
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace mbn_stock
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
private readonly BindingList<SectorHeatMapItem> _items = new BindingList<SectorHeatMapItem>();
|
||||
private readonly BindingList<ColorRangeRule> _colorRules = new BindingList<ColorRangeRule>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,20 @@ namespace mbn_stock
|
||||
|
||||
public static Bitmap Render(IEnumerable<SectorHeatMapItem> sourceItems)
|
||||
{
|
||||
return Render(sourceItems, new Size(ExportWidth, ExportHeight));
|
||||
return Render(sourceItems, new Size(ExportWidth, ExportHeight), CreateDefaultColorRules());
|
||||
}
|
||||
|
||||
public static Bitmap Render(IEnumerable<SectorHeatMapItem> sourceItems, IEnumerable<ColorRangeRule> colorRules)
|
||||
{
|
||||
return Render(sourceItems, new Size(ExportWidth, ExportHeight), colorRules);
|
||||
}
|
||||
|
||||
public static Bitmap Render(IEnumerable<SectorHeatMapItem> sourceItems, Size size)
|
||||
{
|
||||
return Render(sourceItems, size, CreateDefaultColorRules());
|
||||
}
|
||||
|
||||
public static Bitmap Render(IEnumerable<SectorHeatMapItem> sourceItems, Size size, IEnumerable<ColorRangeRule> 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<ColorRangeRule> 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<TreeMapNode> nodes = BuildNodes(items, canvas);
|
||||
DrawTiles(graphics, nodes);
|
||||
DrawTiles(graphics, nodes, rules);
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static List<ColorRangeRule> CreateDefaultColorRules()
|
||||
{
|
||||
return new List<ColorRangeRule>
|
||||
{
|
||||
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<TreeMapNode> BuildNodes(IList<SectorHeatMapItem> 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<TreeMapNode> nodes)
|
||||
private static void DrawTiles(Graphics graphics, IList<TreeMapNode> nodes, IList<ColorRangeRule> 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<ColorRangeRule> 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<ColorRangeRule> 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<ColorRangeRule> NormalizeColorRules(IEnumerable<ColorRangeRule> colorRules)
|
||||
{
|
||||
List<ColorRangeRule> rules = colorRules == null
|
||||
? new List<ColorRangeRule>()
|
||||
: 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'%'"
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ColorRangeRule.cs" />
|
||||
<Compile Include="Form1.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
|
||||
Reference in New Issue
Block a user