using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using TradeIdeas.TIProData; /* This form is based on the top list form in TIProData/SampleProgram. I removed * everything but the grid. And I added the ability to fill in the title of the * window. The data is only configured and requested in the constructor. * * This is simpler than the window we plan to use in TI Pro. It doesn't attach * to the config window. However, there's no good reason why those extra * features couldn't be added here. In that case we could move this window from * the SimpleTIPro application to an appropriate GUI library. */ namespace TradeIdeas.TIProGUI { public partial class TopListForm : Form { private BindingList _boundList = new BindingList(); private TopList _topList; private readonly ConnectionMaster _connectionMaster; public TopListForm(ConnectionMaster connectionMaster, String config) { _connectionMaster = connectionMaster; InitializeComponent(); if (!IsHandleCreated) // This is required because the callbacks can come in any thread. InvokeRequired // does not work correctly before the handle is created. This is necessary // in any window which requests data in the constructor. CreateHandle(); /* Setting the data source allows us to use any type of object we * want to store the row data. If we added the row directly using * dataGridView1.Rows.Add(myRow), myRow would have to be an array * of objects. That means we'd have to parse and format the data * as we were adding it to the data. That's not completely * unreasonble, but I'd rather keep it the way it is. */ dataGridView1.DataSource = _boundList; // A totally empty table is legal, but it looks ugly. dataGridView1.Columns.Add(DataCell.GetSymbolColumn()); _topList = connectionMaster.TopListManager.GetTopList(config); _topList.TopListStatus += new TopListStatus(_topList_TopListStatus); _topList.TopListData += new TopListData(_topList_TopListData); _topList.Start(); Text = "Top List Loading..."; } private IList Columns; private void _topList_TopListStatus(TopList sender) { if (InvokeRequired) BeginInvoke((MethodInvoker)delegate { _topList_TopListStatus(sender); }); else { if ((sender == _topList) && (!_topList.TopListInfo.SameColumns(Columns))) { _primedToFixHeader = true; Columns = _topList.TopListInfo.Columns; dataGridView1.Columns.Clear(); //dataGridView1.Rows.Clear(); _boundList.Clear(); dataGridView1.Columns.Add(DataCell.GetSymbolColumn()); foreach (ColumnInfo columnInfo in _topList.TopListInfo.Columns) dataGridView1.Columns.Add(DataCell.GetColumn(columnInfo, _connectionMaster, this)); // Window if (_topList.TopListInfo.WindowName == "") Text = "Top List"; else Text = _topList.TopListInfo.WindowName; } } } private bool _primedToFixHeader = true; private void SetColumnWidths() { if ((_boundList.Count != 0) && (_primedToFixHeader)) { // The first time we see data after the list is empty we resize all of the columns. // I'm not sure if that's right, but it's reasonable. If we get a change to the // header, perhaps we should reset the primed flag. But it has to be a real change, // not just a repeat of what we've already seen. That will never happen in our first // app, but it could in the real TI Pro. A simpler proxy for this event could be when // we request new data. That is probably reasonable. dataGridView1.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCellsExceptHeader); foreach (DataGridViewColumn column in dataGridView1.Columns) if (column.Width < 40) column.Width = 40; _primedToFixHeader = false; } } void _topList_TopListData(List rows, TopList sender) { if (InvokeRequired) BeginInvoke((MethodInvoker)delegate { _topList_TopListData(rows, sender); }); else { if (sender == _topList) { // Table int index = 0; foreach (RowData row in rows) { if (index >= _boundList.Count) _boundList.Add(row); else _boundList[index] = row; index++; } while (_boundList.Count > index) _boundList.RemoveAt(_boundList.Count - 1); /* This method works, but every time there is new data, you lose the * scroll position and the selection. BindingList newList = new BindingList(); foreach (RowData row in rows) newList.Add(row); dataGridView1.DataSource = newList; * */ /* This method works, but every time there is new data, you lose the * scroll position and the selection. _boundList.Clear(); foreach (RowData row in rows) _boundList.Add(row); * */ SetColumnWidths(); } } } } class IconHeader : DataGridViewColumnHeaderCell { static private readonly Brush BACKGROUND = new SolidBrush(Color.FromArgb(255, 0, 192, 192)); static private readonly Brush HIGHLIGHT = new SolidBrush(Color.FromArgb(255, 0, 252, 252)); static private readonly Brush SHADOW = new SolidBrush(Color.FromArgb(255, 0, 130, 130)); private Image _image; private readonly string _filterCode; private readonly ImageCacheManager _imageCacheManager; private readonly Control _threadControl; public IconHeader(ColumnInfo columnInfo, ConnectionMaster connectionMaster, Control threadControl) { _filterCode = "Min" + columnInfo.InternalCode; _imageCacheManager = connectionMaster.ImageCacheManager; _threadControl = threadControl; ToolTipText = columnInfo.Description + " (" + columnInfo.Units + ')'; _image = _imageCacheManager.GetFilter(_filterCode); if (null == _image) // This might be ineffecient. If we are creating a bunch of tables at once, most of the columns // will need their own callback. Registering and unregistering is O(n) (I think) so startup // could be O(n*n). _imageCacheManager.CachedImageAvailable += ImageCacheManager_CachedImageAvailable; FixImage(); } void ImageCacheManager_CachedImageAvailable() { // Note: We cannot access the datagrid at this time. It is possible to get this callback // before this object has been added to a data grid. That is uncommon -- i've only seen it when // in the debugger -- but it's a race condition none the less. if (_threadControl.InvokeRequired) _threadControl.Invoke((MethodInvoker)delegate { ImageCacheManager_CachedImageAvailable(); }); else { bool imageFound = false; Image newImage = _imageCacheManager.GetFilter(_filterCode); if (newImage != _image) { _image = newImage; imageFound = true; FixImage(); // I couldn't find any better way to invalidate the cell! Value = this; } // This seems like the simplest way to clean up. We're saying that we want to get exactly // one valid copy of the image and after that we don't care. An alternative would be to // try to catch the dispose event. if (imageFound) _imageCacheManager.CachedImageAvailable -= ImageCacheManager_CachedImageAvailable; } } private void FixImage() { if (null == _image) return; if ((_image.Width != ImageCacheManager.ICON_WIDTH) || (_image.Height != ImageCacheManager.ICON_HEIGHT)) { // Not strictly necessary, but why even try to deal with this case. // Really we just need to make sure the image is not too small. _image = null; return; } Bitmap original = _image as Bitmap; if (null == original) { // This shouldn't happen. Perhaps ImageCacheManager should return a bitmap not an image. _image = null; return; } // For simplicity we always trim the border from the original, then redraw the border ourselves. // The border is always 1 pixel. The body might be resized or padded. Rectangle body = new Rectangle(1, 1, ImageCacheManager.ICON_WIDTH - 2, ImageCacheManager.ICON_HEIGHT - 2); _image = original.Clone(body, original.PixelFormat); } protected override Size GetPreferredSize(Graphics graphics, DataGridViewCellStyle cellStyle, int rowIndex, Size constraintSize) { return new Size(ImageCacheManager.ICON_WIDTH, ImageCacheManager.ICON_HEIGHT); } protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates dataGridViewElementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts) { if ((cellBounds.Width < 3) || (cellBounds.Height < 3)) // For simplicity we give up if it's less than 3 pixels. We could do better. We could still // try to draw the border if it's only 2 pixels. But it's not that important. We don't really // care about the tiny cases except that we don't raise an exception. It looks like the user // is not allowed to do this, anyway. base.Paint(graphics, clipBounds, cellBounds, rowIndex, dataGridViewElementState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts & ~DataGridViewPaintParts.ContentForeground); else { graphics.FillRectangle(HIGHLIGHT, cellBounds); Rectangle destination = new Rectangle(cellBounds.X + 1, cellBounds.Y + 1, cellBounds.Width - 1, cellBounds.Height - 1); graphics.FillRectangle(SHADOW, destination); destination.Height = destination.Height - 1; destination.Width = destination.Width - 1; graphics.FillRectangle(BACKGROUND, destination); if (null != _image) { // We do the same thing with the height and the width. If the container is too big, // we center the image. If the image is too big, we shrink the image. if (_image.Width < destination.Width) { // It seems like Rectangle.X and Rectangle.Left mean the same thing, but the latter is readonly. destination.X = destination.X + (destination.Width - _image.Width) / 2; destination.Width = _image.Width; } if (_image.Height < destination.Height) { destination.Y = destination.Y + (destination.Height - _image.Height) / 2; destination.Height = _image.Height; // I tested this by adding // result.HeaderText = "Symbol"; // to GetSymbolColumn(). Normally the row height is exactly what we want so this // code is not executed. } graphics.DrawImage(_image, destination); // This does surprisingly good. It will stretch or shrink the image to the exact size. //graphics.DrawImage(_image,cellBounds); // The following actually does a surprisingly good job. The image is unscaled, and the top left // corner of the image in drawn in the top left corner of the cell. //graphics.DrawImageUnscaled(_image, cellBounds); // Oddly enough, DrawImageUnscaledAndClipped() scales the image and does not clip it. It // does the same thing as DrawImage(image, rectangle); //graphics.DrawImageUnscaledAndClipped(_image, cellBounds); } } } } class DataCell : DataGridViewTextBoxCell { private enum Mode { Price, Number, String }; private Mode _mode; private string _format; private string _internalCode; private string _wireName; public DataCell() { /* This is required! If you don't have a constructor with no arguments, * the code will fail at runtime with a confusing error message. * * Ideally _mode, _format, etc. would all be readonly. But we need to * be able to use Clone(). base.Clone() calls this constructor, then * our version of Clone() updates these member variables. */ } public DataCell(string wireName) { _wireName = wireName; _mode = Mode.String; } public DataCell(ColumnInfo columnInfo) { // A reasonable default. _mode = Mode.String; if (columnInfo.Format == "p") _mode = Mode.Price; else { int digits; if (Int32.TryParse(columnInfo.Format, out digits)) { if (digits > 7) digits = 7; else if (digits < 0) digits = 0; _mode = Mode.Number; _format = "N" + digits; } } _internalCode = columnInfo.InternalCode; // This belongs somewhere else! _wireName = columnInfo.WireName; } public DataGridViewContentAlignment Alignment() { if (_mode == Mode.String) return DataGridViewContentAlignment.MiddleLeft; else return DataGridViewContentAlignment.MiddleRight; } protected override object GetFormattedValue(object value, int rowIndex, ref DataGridViewCellStyle cellStyle, TypeConverter valueTypeConverter, TypeConverter formattedValueTypeConverter, DataGridViewDataErrorContexts context) { BindingList tableData = (BindingList)DataGridView.DataSource; if (rowIndex >= tableData.Count) /* I see this sometimes in SimpleTIPro, but I never saw it in the TIProDataTest * project. I did what I could to make them as similar as possible, but I * still all this difference. * * This seems to be a temporary condition. Sometimes the table asks for the * value of a row just an instant before we can find the row. */ return ""; RowData rowData = tableData[rowIndex]; //return "(" + _wireName + ", " + rowIndex + ")"; // I was planning to cache this next part. But it seems like there's so // much work just getting to here, and then more work rendering the string // on the screen, so this one conversion might not mean much! String cellData = rowData.GetAsString(_wireName); if (_mode == Mode.String) return cellData; // Convert the string we received from the server into a double, then format // it in the appropriate way. It was tempting to have the server do the // formatting for us. But then we might lose precision if we wanted to do // anything else with the value. For example, sorting should work on all the // digits, not just the visible ones. And maybe the user wants to change the // number of digits to display. double cellAsDouble; if (!Double.TryParse(cellData, out cellAsDouble)) return ""; if (_mode == Mode.Number) return cellAsDouble.ToString(_format); else // Mode.Price if (rowData.GetAsString("four_digits") == "1") return cellAsDouble.ToString("N4"); else return cellAsDouble.ToString("N2"); } public override object Clone() { DataCell result = (DataCell)base.Clone(); result._mode = _mode; result._format = _format; result._internalCode = _internalCode; result._wireName = _wireName; return result; } public static DataGridViewColumn GetColumn(String wireName) { return GetColumn(new DataCell(wireName)); } public static DataGridViewColumn GetColumn(ColumnInfo columnInfo, ConnectionMaster connectionMaster, Control threadControl) { DataGridViewColumn result = GetColumn(new DataCell(columnInfo)); result.HeaderCell = new IconHeader(columnInfo, connectionMaster, threadControl); return result; } private static DataGridViewColumn GetColumn(DataCell dataCell) { DataGridViewTextBoxColumn result = new DataGridViewTextBoxColumn(); result.CellTemplate = dataCell; result.DefaultCellStyle = new DataGridViewCellStyle(); // Padding can add space, but not take it away. The default value is 0. Negative // values seem to be treated as 0. //result.DefaultCellStyle.Padding = new Padding(-10); result.DefaultCellStyle.Alignment = dataCell.Alignment(); return result; } public static DataGridViewColumn GetSymbolColumn() { DataGridViewColumn result = GetColumn("symbol"); result.HeaderText = "Symbol"; return result; } } }