0

My program is pretty straight forward in concept - it allows a user to take scores during bowling tournaments, as well as showing the players the scores through a scoreboard.

There's a score sheet form in which they enter scores, and a scoreboard form that shows the scores to the players, by division. The scoreboard is run in a different thread than the main program.

The scoreboard is comprised of a TableLayoutPanel which I manipulate programmatically to represent a table of the scores to show. My issue with this is that it takes a long time (especially for a long list of players) for the table to render. The user experience of watching the table render itself leaves to be desired as well.

I tested the speed of rendering textboxes, labels, and picture boxes to lessen the load; textboxes won, so I changed the score labels to textboxes... but it's still not enough.

It's a lot to look through, but if anyone has any idea how I could further speed up the rendering of the scoreboard, I'd be all ears/eyes.

Here's my process broken down.

The calling method (called every 15 seconds by a timer):

    private void switchBoard()
    {
        night = nights.GetNight(nightID);
        NightDay = night.Night_Date.ToString("ddd");
        //set the id of the division to show
        dtDivisions = scoreBoard.roll_display(dtDivisions);
        //get the row that is set to show
        DataRow[] drs = dtDivisions.Select("showing = 1");
        DataRow dr = drs[0];
        //update the title
        lbl_title.Top = 30;
        lbl_title.Text = (string)dr["title"] + "'s Qualifying - " + NightDay;
        lbl_title.Width = this.Width;
        lbl_title.TextAlign = ContentAlignment.MiddleCenter;
        //SET UP THE TABLE
        //get number of columns (games) for selected night
        int Cols = games.GetCountGamesForTourNightByDivision(tourID, nightID, scoreBoard.ShowDivision) + 3; //ACCOUNT FOR HEADER COLS, RANK AND TOTALS
        //get number of rows (players) for selected night
        int Rows = players.GetCountPlayersForTourNightByDivision(TourID, nightID, scoreBoard.ShowDivision) + 1; //ACCOUNT FOR HEADER ROWS
        //generate the table
        GenerateTable(Cols, Rows);
        //generate the headers
        GenerateHeaders(tourID, nightID);
        //fill in the scores
        GenerateScoreLabels(tourID, nightID, scoreBoard.ShowDivision);
    }

Generating the table:

    private void GenerateTable(int columnCount, int rowCount)
    {
        //Clear out the existing controls, we are generating a new table layout
        this.tblPnlScoreboard.Controls.Clear();

        //Clear out the existing row and column styles
        this.tblPnlScoreboard.ColumnStyles.Clear();
        this.tblPnlScoreboard.RowStyles.Clear();

        //setting up the row and column counts
        this.tblPnlScoreboard.ColumnCount = columnCount;
        this.tblPnlScoreboard.RowCount = rowCount;

        for (int x = 0; x < columnCount; x++)
        {
            //add a column
            if(x==0) //ranking column
            {
                this.tblPnlScoreboard.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute,30));
            }
            else if(x==1) //names
            {
                this.tblPnlScoreboard.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
            }
            else if(x==columnCount-1) //totals
            {
                this.tblPnlScoreboard.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
            }
            else //games
            {
                this.tblPnlScoreboard.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, (this.tblPnlScoreboard.Width - 130) / columnCount));
            }

            for (int y = 0; y < rowCount; y++)
            {
                //add rows.  Only do this once, when creating the first column
                if (x == 0)
                {
                    if(y==0)
                    {
                        this.tblPnlScoreboard.RowStyles.Add(new RowStyle(SizeType.Absolute, 50));
                    }
                    else
                    {
                        this.tblPnlScoreboard.RowStyles.Add(new RowStyle(SizeType.AutoSize));
                    }

                }
            }
        }
    }

Generating the headers:

    private void GenerateHeaders(int TourID, int NightID)
    {
        //get the players to display
        DataTable dtPlayers = players.GetPlayersForTourNightByDivision(tourID, nightID, scoreBoard.ShowDivision);

        int Row = 1; //0 is the header row for Games and so on
        foreach (DataRow dr in dtPlayers.Rows)
        {
            //create the label
            Label lblPlayer = new Label();
            lblPlayer.Name = dr["ID"].ToString(); //name is the ID of the player
            lblPlayer.Text = dr["player_name"].ToString(); //the text is the name of the player
            lblPlayer.BackColor = Color.Transparent;
            lblPlayer.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
            lblPlayer.TextAlign = ContentAlignment.MiddleLeft;
            lblPlayer.AutoSize = true;
            lblPlayer.Height = tblPnlScoreboard.GetRowHeights()[Row];
            //add the label to the table
            this.tblPnlScoreboard.Controls.Add(lblPlayer, 1, Row);

            //create the Total label
            Label lblTotal = new Label();
            lblTotal.Name = "lbl_total"; //name is arbitrary in this context
            lblTotal.Text = dr["Total"].ToString(); //the text is the total
            lblTotal.BackColor = Color.Transparent;
            lblTotal.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
            lblTotal.TextAlign = ContentAlignment.MiddleLeft;
            lblTotal.AutoSize = true;
            lblTotal.Height = tblPnlScoreboard.GetRowHeights()[Row];
            //add the label to the table
            this.tblPnlScoreboard.Controls.Add(lblTotal, tblPnlScoreboard.ColumnCount, Row);

            //increment the row index
            Row++;
        }

        //totals column
        Label lblTotals = new Label();
        //lblTotals.Width = this.tblPnlScoreboard.GetColumnWidths()[this.tblPnlScoreboard.ColumnCount - 1];
        lblTotals.Height = tblPnlScoreboard.GetRowHeights()[0];
        lblTotals.Name = "lbl_total"; //name is the ID of the Game
        lblTotals.Text = "Totals"; //text is the display name of the Game
        lblTotals.BackColor = Color.Transparent;
        lblTotals.ForeColor = Color.White;
        lblTotals.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
        lblTotals.TextAlign = ContentAlignment.MiddleCenter;
        lblTotals.AutoSize = true;
        lblTotals.Anchor = (AnchorStyles.None);
        //add the label to the table
        this.tblPnlScoreboard.Controls.Add(lblTotals, this.tblPnlScoreboard.ColumnCount-1, 0);

        //get the games to display
        DataTable dtGames = games.GetGamesForTourNightByDivision(tourID, nightID, scoreBoard.ShowDivision);

        int Col = 2; //0 is the header column for rank, 1 is the header col for Players
        foreach (DataRow dr in dtGames.Rows)
        {
            //create the label
            Label lblGame = new Label();
            lblGame.Width = this.tblPnlScoreboard.GetColumnWidths()[Col];
            lblGame.Height = tblPnlScoreboard.GetRowHeights()[0];
            lblGame.Name = dr["ID"].ToString(); //name is the ID of the Game
            lblGame.Text = dr["disp_name"].ToString().Replace("Game ", ""); //text is the display name of the Game
            lblGame.BackColor = Color.Transparent;
            lblGame.ForeColor = Color.White;
            lblGame.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
            lblGame.TextAlign = ContentAlignment.MiddleCenter;
            lblGame.Anchor = (AnchorStyles.None);
            //add the label to the table
            this.tblPnlScoreboard.Controls.Add(lblGame, Col, 0);
            //increment the column index
            Col++;
        }
    }

Finally, generating the scores:

    private void GenerateScoreLabels(int TourID, int NightID, int DivID)
    {
        //get the id of the playergames record
        //expl: each player/game pair has a unique ID - these IDs will be used to update the scores
        Players players = new Players();
        DataTable dtScores = players.GetPlayerGamesIDsForTourNightByDivision(TourID, NightID, scoreBoard.ShowDivision);
        Divisions Divs = new Divisions();
        DataTable dtColors = Divs.GetDivisionScoreboardColors(DivID);

        foreach (DataRow dr in dtScores.Rows)
        {
            //find the coordinates in the table, where the textbox should be added
            int col = FindX((int)dr["fk_game_id"]);
            int row = FindY((int)dr["fk_player_id"]);

            if (col > 0 && row > 0)
            {                   
                TextBox txt_score = new TextBox();
                txt_score.Name = dr["ID"].ToString(); //name of the control is the player/game ID
                txt_score.Text = dr["score"].ToString(); //the text in the control is the score
                txt_score.ForeColor = Color.Black;
                txt_score.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
                txt_score.Width = this.tblPnlScoreboard.GetColumnWidths()[col];
                txt_score.Height = tblPnlScoreboard.GetRowHeights()[0];
                if(row % 2 == 0)
                {
                    txt_score.BackColor = Color.FromArgb((int)dtColors.Rows[0]["sb_even_row_color"]);
                }
                else
                {
                    txt_score.BackColor = Color.FromArgb((int)dtColors.Rows[0]["sb_odd_row_color"]);
                }
                txt_score.BorderStyle = BorderStyle.None;
                txt_score.TextAlign = HorizontalAlignment.Center;
                txt_score.Anchor = (AnchorStyles.Top);

                this.tblPnlScoreboard.Controls.Add(txt_score, col, row);

                //start the switchboard timer
                ttmr_switch.Enabled = true;
            }
        }
    }

On the CellPaint event of the TableLayoutPanel, I have these processes:

    private void tblPnlScoreboard_CellPaint(object sender, TableLayoutCellPaintEventArgs e)
    {
        Graphics g = e.Graphics;
        Rectangle r = e.CellBounds;
        SolidBrush sb = GetBrushFor(e.Row, e.Column, scoreBoard.ShowDivision);

        g.FillRectangle(sb, r);
        sb.Dispose();
    }

Selection of Colors:

    private SolidBrush GetBrushFor(int row, int column, int DivID)
    {
        DataTable dt_colors = divisions.GetDivisionScoreboardColors(DivID);

        if (row == 0)
        {   //column headers
            SolidBrush brush = new SolidBrush(Color.FromArgb((int)dt_colors.Rows[0]["sb_column_header_color"]));
            return brush;
        }
        else
        {   
            if(row % 2 == 0) //even row
            {
                SolidBrush brush = new SolidBrush(Color.FromArgb((int)dt_colors.Rows[0]["sb_even_row_color"]));
                return brush;
            }
            else //odd row
            {
                SolidBrush brush = new SolidBrush(Color.FromArgb((int)dt_colors.Rows[0]["sb_odd_row_color"]));
                return brush;
            }
        }
    }
wribit
  • 605
  • 1
  • 6
  • 18
  • You have a lot of cross-threading calls there (depending on the Timer used), updating UI elements should only take place in the UI thread. You also aren't disposing of your GDI resources when you are finished with them. – Ron Beyer Aug 18 '15 at 12:54
  • 1
    What about using a DataGridView instead? You could use the DataTable as DataSource, and you should just configure some TextBox columns. – György Kőszeg Aug 18 '15 at 12:58
  • The timer is a system.Timer... what do you mean by GDI resources? I'm fairly new to this. – wribit Aug 18 '15 at 12:58
  • Yes, if you are using System.Threading.Timer then you have a cross-thread call issue. GDI resources are the SolidBrush, when you are finished with it, you should call its `.Dispose` method. This requires breaking out your call to `GetBrushFor` and storing it in a temporary variable then disposing it after the call to `FillRectangle`. – Ron Beyer Aug 18 '15 at 12:59
  • @taffer I thought of using a DataGridView as well... but it just didn't allow me the flexibility I needed to show the table the way the client wanted it. – wribit Aug 18 '15 at 13:00
  • @Ron Beyer So in your opinion a straight forward Form.Timer would be best? – wribit Aug 18 '15 at 13:02
  • @RonBeyer I changed my timer to a Form.Timer as well as disposing of the SolidBrush. Although this seems to have made it slightly smoother, the time it took to render a table of 15 players was the same as before. – wribit Aug 18 '15 at 13:18
  • Is it much faster without the `tblPnlScoreboard_CellPaint` call? Doing the custom cell painting I'm sure slows it down. Maybe make the Brush's static to avoid newing a Brush every time. – SwDevMan81 Aug 18 '15 at 13:28
  • Are you redrawing the entire table every timer tick event? How often is the timer firing? – Ron Beyer Aug 18 '15 at 13:43
  • @SwDevMan81 Indeed, it goes much faster without the cellpaint event. I'm in the process of trying to achieve the same effect as painted rows, by controlling the back color of the controls I place in the table. If I can make it look as good, the client will be happy – wribit Aug 18 '15 at 13:49
  • Using TableLayoutPanel to create your own grid control is never not a mistake. You simply have entirely too many windows in your UI. Painting and resizing become a plodding affair with noticeable rendering delays. If ListView or DataGridView don't cut the mustard then you need to go shopping, any component vendor has a grid control. – Hans Passant Aug 18 '15 at 15:09
  • @HansPassant [again](http://stackoverflow.com/a/22940313/643085)? Why don't you recommend the OP to use [proper technology](http://stackoverflow.com/a/23023207/643085) (WPF) instead of spending thousands in third party controls? Are you affiliated with Telerik? – Federico Berasategui Aug 18 '15 at 16:01
  • @wribit **Solution:** use WPF. winforms is completely useless. You can achieve this very easily in WPF, having support for millions of rows of data, with an immediate response time, thanks to WPF's built in UI virtualization. You can integrate WPF content in your existing winforms project using an `ElementHost`. Have a look at the link in my previous comment for a concrete, full working example on this. – Federico Berasategui Aug 18 '15 at 16:04
  • @HighCore Thanks, I'm reading through WPF a bit now. I'm new to C# and felt comfortable starting with winForms - if you say both WPF and winForms can be used together I'm sold – wribit Aug 18 '15 at 17:29
  • How many players and games (i.e. rows and columns) are you targeting? – Ivan Stoev Aug 18 '15 at 18:50

2 Answers2

1

Some people suggested you to use a "proper technology". I would rather say "use properly the technology". Even this weird (sorry) design/implementation choice can be made to work much faster, as shown in the code below, which as you may see is handling rebuilding a table containing 100 rows x 10 columns 10 times per sec - not a big deal compared to professional grids, but far from the original implementation. Key points:
1. Enclose table rebuild with Suspend/ResumeLayout to avoid intensive recalculations during the process.
2. Use custom double buffered TableLayoutPanel to avoid flickering.
3. Custom paint the data cells to avoid allocating a lot of controls.

Since essential data related parts are missing in the code you gave us, I can't provide you exactly the same working code. Hope you can recognize and map it to your stuff.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace Tests
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new ScoreBoardForm { WindowState = FormWindowState.Maximized });
        }
    }

    class ScoreBoardForm : Form
    {
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            players = new List<Player>();
            for (int i = 0; i < 100; i++)
                players.Add(new Player { ID = i + 1, Name = "P" + (i + 1), Total = random.Next(1000) });
            games = new List<Game>();
            for (int i = 0; i < 10; i++)
                games.Add(new Game { ID = i + 1, Name = "G" + (i + 1) });

            scoreBoardTable = new ScoreBoardTable { Dock = DockStyle.Fill, Parent = this };
            scoreBoardTable.Bounds = this.DisplayRectangle;
            UpdateScoreBoard();
            scoreBoardTable.CellPaint += OnScoreBoardTableCellPaint;

            updateTimer = new Timer { Interval = 100 };
            updateTimer.Tick += (_sender, _e) => UpdateScoreBoard();
            updateTimer.Start();
        }
        private void OnScoreBoardTableCellPaint(object sender, TableLayoutCellPaintEventArgs e)
        {
            int playerIndex = e.Row - 1, gameIndex = e.Column - 2;
            if (playerIndex >= 0 && playerIndex < players.Count && gameIndex >= 0 && gameIndex < games.Count)
            {
                using (var br = new SolidBrush(GetBackColor(e.Row)))
                    e.Graphics.FillRectangle(br, e.CellBounds);
                var score = GetScore(players[playerIndex], games[gameIndex]);
                var sf = new StringFormat { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Center };
                e.Graphics.DrawString(score.ToString(), defaultCellFont, Brushes.Black, e.CellBounds, sf);
            }
        }
        private int GetScore(Player player, Game game)
        {
            return random.Next(10000);
        }
        class ScoreBoardTable : TableLayoutPanel
        {
            public ScoreBoardTable() { DoubleBuffered = AutoScroll = true; }
        }
        TableLayoutPanel scoreBoardTable;
        Timer updateTimer;
        List<Player> players;
        List<Game> games;
        Random random = new Random();
        class Player
        {
            public int ID;
            public string Name;
            public int Total;
        }
        class Game
        {
            public int ID;
            public string Name;
        }
        private void UpdateScoreBoard()
        {
            scoreBoardTable.SuspendLayout();
            GenerateTable(games.Count + 3, players.Count + 1);
            GenerateHeaderCells();
            // Custom cell paint is much faster, but requires a good data model.
            // If you uncomment the following line, make sure to get rid of CellPaint. 
            //GenerateScoreCells();
            scoreBoardTable.ResumeLayout(true);
        }
        private void GenerateTable(int columnCount, int rowCount)
        {
            scoreBoardTable.Controls.Clear();
            scoreBoardTable.ColumnStyles.Clear();
            scoreBoardTable.RowStyles.Clear();
            scoreBoardTable.ColumnCount = columnCount;
            scoreBoardTable.RowCount = rowCount;

            // Columns
            // Ranking
            scoreBoardTable.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 30));
            // Name 
            scoreBoardTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
            // Games
            var percent = (columnCount - 3) / (float)columnCount;
            for (int col = 2; col < columnCount - 1; col++)
                scoreBoardTable.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, percent));
            // Totals
            scoreBoardTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));

            // Rows
            // Header
            scoreBoardTable.RowStyles.Add(new RowStyle(SizeType.Absolute, 50));
            // Players
            for (int row = 1; row < rowCount; row++)
                scoreBoardTable.RowStyles.Add(new RowStyle(SizeType.AutoSize));
        }
        private void GenerateHeaderCells()
        {
            Color backColor = Color.DarkGray, foreColor;
            int row, col;
            // Header
            row = 0;
            foreColor = Color.White;
            col = 0;
            AddCell(row, col++, "rank", "", foreColor, backColor);
            AddCell(row, col++, "playerName", "Player", foreColor, backColor);
            foreach (var game in games)
                AddCell(row, col++, "gameName" + game.ID, game.Name, foreColor, backColor);
            AddCell(row, col, "totalColumn", "Totals", foreColor, backColor);
            // Rows
            foreColor = Color.Black;
            row++;
            foreach (var player in players)
            {
                backColor = GetBackColor(row);
                AddCell(row, 0, "playerRank_" + player.ID, "", foreColor, backColor, ContentAlignment.MiddleLeft);
                AddCell(row, 1, "playerName_" + player.ID, player.Name, foreColor, backColor, ContentAlignment.MiddleLeft);
                AddCell(row, scoreBoardTable.ColumnCount, "playerTotal_" + player.ID, player.Total.ToString(), foreColor, backColor, ContentAlignment.MiddleRight);
                row++;
            }
        }
        private void GenerateScoreCells()
        {
            var foreColor = Color.Black;
            int row = 1;
            foreach (var player in players)
            {
                var backColor = GetBackColor(row);
                int col = 2;
                foreach (var game in games)
                {
                    var score = GetScore(player, game);
                    AddCell(row, col, "score_" + player.ID + "_" + game.ID, score.ToString(), foreColor, backColor, ContentAlignment.MiddleRight, false);
                    col++;
                }
                row++;
            }
        }
        static readonly Font defaultCellFont = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
        private Label AddCell(int row, int col, string name, string text, Color foreColor, Color backColor, ContentAlignment textAlign = ContentAlignment.MiddleCenter, bool autoSize = true)
        {
            var label = new Label();
            label.Name = name;
            label.Text = text;
            label.BackColor = backColor;
            label.ForeColor = foreColor;
            label.Font = defaultCellFont;
            label.TextAlign = textAlign;
            label.AutoSize = autoSize;
            label.Margin = new Padding(0);
            label.Dock = DockStyle.Fill;
            scoreBoardTable.Controls.Add(label, col, row);
            return label;
        }
        static Color GetBackColor(int row)
        {
            if (row % 2 == 0) //even row
                return Color.Yellow;
            else //odd row
                return Color.LightGreen;
        }
    }
}

EDIT And here is equivalent implementation using DataGridView (note that now the number of rows (players) are ten times more at the same refresh rate):

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace Tests
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new ScoreBoardForm { WindowState = FormWindowState.Maximized });
        }
    }

    class ScoreBoardForm : Form
    {
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            players = new List<Player>();
            for (int i = 0; i < 1000; i++)
                players.Add(new Player { ID = i + 1, Name = "P" + (i + 1), Total = random.Next(1000) });
            games = new List<Game>();
            for (int i = 0; i < 10; i++)
                games.Add(new Game { ID = i + 1, Name = "G" + (i + 1) });

            InitScoreBoard();
            UpdateScoreBoard();

            updateTimer = new Timer { Interval = 100 };
            updateTimer.Tick += (_sender, _e) => UpdateScoreBoard();
            updateTimer.Start();
        }

        DataGridView scoreBoardTable;
        Timer updateTimer;
        List<Player> players;
        List<Game> games;
        Random random = new Random();
        class Player
        {
            public int ID;
            public string Name;
            public int Total;
        }
        class Game
        {
            public int ID;
            public string Name;
        }
        private int GetScore(Player player, Game game)
        {
            return random.Next(10000);
        }
        void InitScoreBoard()
        {
            scoreBoardTable = new DataGridView { Dock = DockStyle.Fill, Parent = this };
            scoreBoardTable.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
            scoreBoardTable.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
            scoreBoardTable.MultiSelect = false;
            scoreBoardTable.CellBorderStyle = DataGridViewCellBorderStyle.None;
            scoreBoardTable.BackgroundColor = Color.Honeydew;
            scoreBoardTable.ForeColor = Color.Black;
            scoreBoardTable.AllowUserToAddRows = scoreBoardTable.AllowUserToDeleteRows = scoreBoardTable.AllowUserToOrderColumns = scoreBoardTable.AllowUserToResizeRows = false;
            scoreBoardTable.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
            scoreBoardTable.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCells;
            scoreBoardTable.RowHeadersVisible = false;
            scoreBoardTable.EnableHeadersVisualStyles = false;
            var style = scoreBoardTable.DefaultCellStyle;
            style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
            style = scoreBoardTable.ColumnHeadersDefaultCellStyle;
            style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
            style.BackColor = Color.Navy;
            style.ForeColor = Color.White;
            style = scoreBoardTable.RowHeadersDefaultCellStyle;
            style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
            style = scoreBoardTable.RowsDefaultCellStyle;
            style.SelectionForeColor = style.ForeColor = Color.Black;
            style.SelectionBackColor = style.BackColor = Color.Yellow;
            style = scoreBoardTable.AlternatingRowsDefaultCellStyle;
            style.SelectionForeColor = style.ForeColor = Color.Black;
            style.SelectionBackColor = style.BackColor = Color.LightGreen;
            scoreBoardTable.CellFormatting += OnScoreBoardCellFormatting;
        }
        private void UpdateScoreBoard()
        {
            scoreBoardTable.ColumnCount = 3 + games.Count;
            for (int c = 0; c < scoreBoardTable.ColumnCount; c++)
            {
                var col = scoreBoardTable.Columns[c];
                if (c == 0)
                {
                    col.Name = "Rank";
                    col.HeaderText = "";
                    col.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;
                    col.Width = 48;
                }
                else if (c == 1)
                {
                    col.Name = "Player";
                    col.HeaderText = "Player";
                    col.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft;
                }
                else if (c == scoreBoardTable.ColumnCount - 1)
                {
                    col.Name = "Totals";
                    col.HeaderText = "Totals";
                    col.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
                    //col.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
                }
                else
                {
                    var game = games[c - 2];
                    col.Name = "Game_" + game.ID;
                    col.HeaderText = game.Name;
                    col.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
                    //col.AutoSizeMode = DataGridViewAutoSizeColumnMode.DisplayedCells;
                }
            }
            scoreBoardTable.RowCount = players.Count;
            scoreBoardTable.AutoResizeColumnHeadersHeight();
            scoreBoardTable.Invalidate();
        }
        private void OnScoreBoardCellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
        {
            var player = players[e.RowIndex];
            int col = e.ColumnIndex;
            if (col == 0)
                e.Value = "";
            else if (col == 1)
                e.Value = player.Name;
            else if (col == scoreBoardTable.ColumnCount - 1)
                e.Value = player.Total.ToString();
            else
            {
                var game = games[col - 2];
                e.Value = GetScore(player, game).ToString();
            }
            e.FormattingApplied = true;
        }
    }
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Thank you for putting so much time into this. I will review your code and look into it for sure. I'm new to C# so a lot of it is over my head right now, but with a bit of research I'm sure I'll get this right. – wribit Aug 19 '15 at 11:34
  • I also wanted to ask you what you would have chosen to use as far as implementation if the client wanted a scoreboard that looks very much like a powerpoint presentation of colored tables. I'd be curious to try other implementations – wribit Aug 19 '15 at 11:35
  • 1
    I would research controls which are intended to present tabular data. From standard ones - DataGridView and ListView. Then 3rd parties if buying is an option. If no one works, I would write my own from scratch. It should not be so hard - little calculations and drawing - you don't need to be expert for this, `FillRectangle` and `DrawString` would be sufficient. Anyway, to satisfy your curiosity, I'll put a sample DataGridView based implementation. Enjoy! – Ivan Stoev Aug 19 '15 at 18:56
  • @IvanStoev nice, but doing with with WPF takes half the amount of code, works better (even with 10ms Timer as opposed to 100 as yours), and does not make your code immediately useless by definition because WPF encourages separation of UI and data. – Federico Berasategui Aug 19 '15 at 19:24
  • @HighCore First, that's the OP design choice, not mine. Second, sorry, but I have to disagree - neither separation of UI and data has been invented in WPF nor their mixture has been tolerated in WF. My point was (and still is) that one can do it right or wrong regardless of the technology. And third, if follow your logic, then he should switch to some technology used by the game developers because it will be the fast (you don't really believe games are written in WPF right:-)) Peace please, the question is WF related, so was the answer. – Ivan Stoev Aug 19 '15 at 21:57
  • @IvanStoev none of that changes the fact that all the winforms code you write is immediately useless, because I can't be ported outside of winforms to any other platforms (such as UWP, which is XAML, just like WPF). Also, none of that changes the fact that winforms is a waste of time because it needs all sorts of stupid hacks for everything, whereas WPF has proper DataBinding, which means I can write whatever you do in winforms in 50% the amount of code and 25% the effort. – Federico Berasategui Aug 19 '15 at 22:00
  • @HighCore Then do it my friend and be happy. But you have to accept the fact that this is just your opinion and you can't force everyone to share your WPF deification. – Ivan Stoev Aug 19 '15 at 22:56
  • @HighCore Repeating "proper" and "useless" doesn't prove your point. One can ask you the same - why waste time with XAML instead of HML5. Anyway, I get bored and I don't think here is the place for such a discussions. Also I see a lot of new questions tagged `wpf`, so... – Ivan Stoev Aug 19 '15 at 23:46
  • @IvanStoev `why waste time with XAML instead of HML5` - LOL, nice argument. It really really demonstrates that winforms is a really capable, modern, rich technology that people should choose to build .Net Windows apps. Goodbye. – Federico Berasategui Aug 19 '15 at 23:49
  • @HighCore Yeah, that was kind of offtopic. At least we agree for something - LOL. Bye. – Ivan Stoev Aug 20 '15 at 00:00
  • wribit, regardless of my little conceptual fight with @HighCore, i would recommend you to consider his advice and take a look at WPF if you can afford. unfortunately i can't help there. good luck. – Ivan Stoev Aug 20 '15 at 16:19
0

Well, the CellPaint event was the culprit. I was able to achieve what I wanted by negating the CellPaint event, and instead manipulating the controls in the panel so as to have the right background colors as well as size so they fill the grid.

Thank you for all your comments - they were very helpful. As per HighCore's comment, I will now be looking into WPF and I might be able to get something even slicker.

wribit
  • 605
  • 1
  • 6
  • 18