public int Compare(object x, object y) { if (x is SpreadsheetCell && y is SpreadsheetCell) { SpreadsheetCell cell1 = (SpreadsheetCell)x; SpreadsheetCell cell2 = (SpreadsheetCell)y; // If the two rows are the same, compare the column. int compare = cell1.RowIndex.CompareTo(cell2.RowIndex); return(compare == 0 ? cell1.ColumnIndex.CompareTo(cell2.ColumnIndex) : compare); } throw new Exception("The method or operation is not implemented for these types."); }
public override void Reset() { // This variable is used to give each row a unique index as they are added. It will count monotonically until the // table is reset. Rows and columns are ordered to give each cell a cartesean [RowIndex, ColumnIndex] type of // address. this.rowIndex = 0; // This is the default action for selecting objects in the view. this.SelectionMode = DefaultSpreadsheet.SelectionMode; // The data in the table is ordered according to the primary key. However, the displayed data is ordered by a // user-selectable order and can filter out data based on rules. This filter and sort fields control the data that // is displayed in the viewer. this.spreadsheetRowView.RowFilter = string.Empty; this.spreadsheetRowView.Sort = string.Empty; // Release any of the graphical objects in the current spreadsheet. foreach (SpreadsheetRow spreadsheetRow in this.Rows) { foreach (SpreadsheetColumn spreadsheetColumn in this.Columns) { SpreadsheetCell spreadsheetCell = spreadsheetRow[spreadsheetColumn]; if (spreadsheetCell.Value is Bitmap) { ((Bitmap)spreadsheetCell.Value).Dispose(); } } } this.ViewColumns.Clear(); this.styleTable.Clear(); // A default style is always available. Style defaultStyle = new Style(); defaultStyle.Id = DefaultSpreadsheet.StyleId; this.styleTable.Add(defaultStyle.Id, defaultStyle); // The old animation list is no longer valid after reading a new document. Note that there is an implicit Mutex in the // animated list because a there is a background thread that also uses this list. this.animatedList.Clear(); this.headerRectangle = Rectangle.Empty; this.displayRectangle = Rectangle.Empty; base.Reset(); }
/// <summary> /// Removes the AnimatedCell from the animation task. /// </summary> /// <param name="spreadsheetCell">The cell to be removed.</param> public void Remove(SpreadsheetCell spreadsheetCell) { try { // Make sure the list is locked before removing the cell. Note that the list is sorted, so the binary search picks // up the element quickly and removing the item using the index into the array instead of the reference is quicker // still. this.mutex.WaitOne(); int index = this.arrayList.BinarySearch(spreadsheetCell, this.cellOrderComparer); if (index >= 0) { this.arrayList.RemoveAt(index); } } finally { this.mutex.ReleaseMutex(); } }
/// <summary> /// Add an address to the list of animated cells. /// </summary> /// <param name="spreadsheetCell">A Cell Address to be animated.</param> /// <returns>The cell that was added to the list.</returns> public SpreadsheetCell Add(SpreadsheetCell spreadsheetCell) { try { // Make sure no other threads modify the list while an address is being added. this.mutex.WaitOne(); // The list must be ordered to prevent thrashing during the animation. int index = this.arrayList.BinarySearch(spreadsheetCell, this.cellOrderComparer); if (index < 0) { this.arrayList.Insert(~index, spreadsheetCell); } // This cell will now be animated. return(spreadsheetCell); } finally { // Other threads can now use the list. this.mutex.ReleaseMutex(); } }
/// <summary> /// Provides highlighting of modified cells that fade over time. /// </summary> private void AnimationThread() { // This loop will use a timer to cycle through all the styles associated with an animated cell. When an animation // attribute is declared for a Font style, an array of styles with colors blending from the start color to the actual // cell color is created. As the timer ticks off, those styles are selected for the cell in sequence, giving the // effect of a burst to the 'StartColor' and a gradual fade to the original color of the style. while (this.IsAnimationRunning) { // This region will be filled in with the rectangle of any cells that have changed color. bool hasUpdatedCells = false; Region region = new Region(); region.MakeEmpty(); try { // IMPORTANT CONCEPT: Make sure that the data model of the viewer doesn't change while modifying the styles on // the cells that are being animated. The list of animated cells must also be locked because the XML Reader // can add items to the animated list. BE CAREFUL not to change the order of the locks. If the locks are not // always aquired in the same order, the potential for a deadlock exists. this.readerWriterLock.AcquireWriterLock(Timeout.Infinite); this.animatedList.Mutex.WaitOne(); // Cycle through all the cells that have been animated. The index is used instead of the iterator so the // animation can be removed from the list when it has cycled through all its colors. for (int animatedCellIndex = 0; animatedCellIndex < this.animatedList.Count; animatedCellIndex++) { // The 'animatedList' is a ordered list of row and column addresses, along with members that track the // series of animated styles. The ordering of the list of animated cells is important because there can be // many changes on a document and it's important to quickly access them when purging them. SpreadsheetCell spreadsheetCell = this.animatedList[animatedCellIndex]; // The spreadsheet cell addressed by the animatedCell will have its style changed to the next color in the // sequence. When all the styles (that contain different colors) have been used, the color of the cell // will return to the normal color of the cell. When this happens, the cell is removed from the list of // animated cells. Keeping the list small is important for performance. spreadsheetCell.Style = spreadsheetCell.AnimationArray[spreadsheetCell.AnimationIndex++]; if (spreadsheetCell.AnimationIndex == spreadsheetCell.AnimationArray.Length) { this.animatedList.Remove(spreadsheetCell); } // This region will update all the modified cells on the screen when the loop is finished. if (spreadsheetCell.ColumnViewIndex != int.MinValue) { hasUpdatedCells = true; region.Union(spreadsheetCell.DisplayRectangle); } } } finally { // The viewer's data and the animated list can be use by other threads now. this.readerWriterLock.ReleaseWriterLock(); this.animatedList.Mutex.ReleaseMutex(); } // Broadcast a message to update the screen if there were any cells updated with new styles. if (hasUpdatedCells) { OnDataInvalidated(region); } // This provides the time dimension for the animation. Thread.Sleep(Spreadsheet.animationPeriod); } }
/// <summary> /// Measures the document, row and cell dimensions. /// </summary> /// <returns>A region of the document that has changed.</returns> internal Region MeasureData() { // This area will collect all the rectangles that need to be updated in the viewer. Region region = new Region(); region.MakeEmpty(); // This is used to collect smaller row and cell rectangles into the largest possible area. The idea is to minimzed the // number of rectangles that the GDI has to handle when the document is redrawn. Of course, this could be done with an // infinite region, but the idea is also to minimize the amount of drawing that needs to be done. Rectangle aggregateRectangle = Rectangle.Empty; // This is used as a relatively static area where the dimensions of a row in the current view are calcualted. Rectangle rowRectangle = Rectangle.Empty; // These provide cursors for keeping track of the location as the document is measured. Point displayCursor = Point.Empty; Point printerCursor = Point.Empty; // Initialize the total area of the screen and printed document. The width of the documents has been calculated by // this point (it is calculated in 'MeasureHeader', but the length has not yet been calculated. this.displayRectangle = new Rectangle(0, 0, this.headerRectangle.Width, 0); // The default view defines which rows are visible and in what order the appear. It defines the 'visible' document. // This code will run through the visible document and assign coordinates to the rows and cells according to their // place in the view. int rowIndex = 0; foreach (DataRowView dataRowView in this.spreadsheetRowView) { // The underlying row will be updated with coordinates that represent its location in the view. SpreadsheetRow spreadsheetRow = (SpreadsheetRow)dataRowView.Row; // This index gives the row position within the sorted and filtered view. spreadsheetRow.RowViewIndex = rowIndex++; // This define the coordinates of this row in the view. rowRectangle.X = displayCursor.X; rowRectangle.Y = displayCursor.Y; rowRectangle.Width = this.displayRectangle.Width; rowRectangle.Height = spreadsheetRow.rectangle.Height; // The code below will check the coordinates of the rows and cells with the goal of preserving the data that hasn't // changed. However, if the row is invalid, there's no need to add the cells to the update region. If every cell // of every row were entered into the region, it would choke the GDI. If the row is out of place, this flag will // skip the cell checking logic. bool isRowValid = true; // If the position and height of the row don't agree with where it should be in the DataView, then the coordinates // will have to be updated and the row will be added to the list of things that need redrawing. if (spreadsheetRow.rectangle.Location != rowRectangle.Location || spreadsheetRow.rectangle.Height != rowRectangle.Height || !spreadsheetRow.IsVisible) { // The entire row will be added to the update region, there's no need to add the cell rectangles as well. // This is done as an optimization; if ever cell in a large document were modified at once, it would blow out // the storage are for a region. isRowValid = false; // Update the coordinates of the row with the calculated rectangle from the view. spreadsheetRow.rectangle = rowRectangle; // This will combine the rectangles when the top and bottom borders coincide. The aggregate rectangle collects // individual rows into one large rectangle and places the largest contiguous rectangle possible into the // region. This simplifies the work the GDI has to do to process the invalid areas of the document. if (aggregateRectangle.Left == rowRectangle.Left && aggregateRectangle.Width == rowRectangle.Width && aggregateRectangle.Bottom == rowRectangle.Top) { aggregateRectangle = Rectangle.Union(aggregateRectangle, rowRectangle); } else { if (!aggregateRectangle.IsEmpty) { region.Union(aggregateRectangle); } aggregateRectangle = rowRectangle; } } // This will calculate the location of each of the cells in document coordinates. If the coordinates are // different from the current coordinates that cell will be added to the update region, assuming the row hasn't // already been added. foreach (SpreadsheetColumn spreadsheetColumn in this.ViewColumns) { SpreadsheetCell spreadsheetCell = spreadsheetRow[spreadsheetColumn]; // This is the rectangle that this cell should occupy. Rectangle cellRectangle = new Rectangle(displayCursor, new Size(spreadsheetColumn.Rectangle.Width, spreadsheetRow.rectangle.Height)); // This is the test to see if this cell needs to be redrawn. If the row isn't valid, then we don't need // to redraw the cell because it will be redrawn when the row is painted. If the dimensions of the cell // have changed, then it needs to be repainted. Finally, if there was an original version of this cell and // it isn't the same as the suggested cell, it needs to be repainted. if (isRowValid && (spreadsheetCell.DisplayRectangle != cellRectangle || spreadsheetCell.IsModified)) { // This will aggregate the cells into larger rectangles if their edges cooincide. The large contiguous // rectangles are added to the area that needs to be redrawn. if (aggregateRectangle.Top == cellRectangle.Top && aggregateRectangle.Height == cellRectangle.Height && aggregateRectangle.Right == cellRectangle.Left) { aggregateRectangle = Rectangle.Union(aggregateRectangle, cellRectangle); } else { if (!aggregateRectangle.IsEmpty) { region.Union(aggregateRectangle); } aggregateRectangle = cellRectangle; } } spreadsheetCell.DisplayRectangle = cellRectangle; // This flag is set after the cell is measured. The cell won't be updated again until this flag is set // again. spreadsheetCell.IsModified = false; // Move the cursor up to the next column and test the next cell. displayCursor.X += spreadsheetColumn.Rectangle.Width; } // This will reset the cursor for the next row. displayCursor.X = 0; displayCursor.Y += spreadsheetRow.rectangle.Height; } // If all the rows and all the cells have been measured and checked for changes, then check one last time to see if // there is anything in the aggregate rectangel that needs to be redrawn. if (!aggregateRectangle.IsEmpty) { region.Union(aggregateRectangle); } // This is the height of the displayed document. this.displayRectangle.Height = displayCursor.Y; // This contains all the areas that need to be redrawn. return(region); }