Работа с горизонтальной и вертикальной полосой прокрутки на форме C#

Разработка на C# Просмотров: 1689

User Rating: 0 / 5

Всем привет, друзья.

В сегодняшней статье мы посмотрим на один из возможных вариантов работы с горизонтальной и вертикальной полосой прокрутки на форме в приложении C# для Windows Forms. Полосу прокрутки также часто называют скроллбаром.

В этой статье будет представлен вариант размещения горизонтальной и вертикальной полосы прокрутки непосредственно на главной форме приложения, без использования каких-либо других дочерних элементов, расположенных на форме.

Для демонстрации работы скроллбаров мы напишем простое приложение, которое будет рисовать на главной форме 4000 квадратных клеток, каждая из которых будет пронумерована для удобства. В конце этой статьи я также дам ссылку на готовый архив с примером.

Максимальное количество клеток по горизонтали будет 80, по вертикали - 50 штук. При желании вы сможете потом настроить количество клеток по горизонтали/вертикали по своему вкусу, но общая задумка рассматриваемого в статье примера в том, чтобы все нарисованные на форме клетки не могли все сразу поместиться на главной форме приложения. То есть, мы будем использовать горизонтальный и вертикальный скроллбары на главной форме для того, чтобы переместиться в любую область нарисованных клеток и иметь возможность увидеть все клетки.

Концепция этого простого примера может быть полезна в тех случаях, когда вместо обычных пронумерованных клеток вам потребуется производить отрисовку каких-либо других объектов в ваших программах на C#, и количество этих объектов не будет сразу помещаться в текущую область видимости формы.

Давайте сразу посмотрим на тот результат, который у нас получится на выходе из данной статьи.

При запуске нашей тестовой программы главная форма приложения будет выглядеть так:

 

Как видим, слева-вверху номер самой первой клетки равен 1, затем нумерация клеток идёт по горизонтали и вниз. Также можно видеть, что при первом запуске формы оба скроллбара - горизонтальный и вертикальный - видны, и их позиция равна 0, т.е. они находятся в своём начальном положении.

Если мы передвинем бегунок горизонтального скроллбара в крайнее правое положение, то форма преобразится следующим образом:

Аналогичным образом, если мы сдвинем бегунок вертикального скроллбара вниз до крайней позиции, то мы увидим самую последнюю клетку нашего "поля", номер которой равен 4000 (вспомним, что общее количество клеток у нас 50 x 80 = 4000):

Итак, приступим к написанию самой тестовой программы.

Создание нового проекта в среде Microsoft Visual Studio

Откройте среду разработки Microsoft Visual Studio и создайте новый проект с типом Приложение Windows Forms (.NET Framework), как показано ниже:

В качестве имени нового проекта задайте ScrollbarExample и выберите местоположение, куда будут сохранены файлы нового проекта (здесь я обычно оставляю все настройки по умолчанию и рекомендую сделать то же самое, просто запомните каталог, указанный в поле "Расположение", чтобы затем открывать его в Проводнике).

После создания проекта будет создана главная форма по умолчанию с именем Form1 и соответствующий ей класс Form1.cs.

Выберите в окне "Обозреватель решений" эту форму и в окне "Свойства" измените название класса для формы на FrmScrollbarExample.cs.

В диалоговом окне, запрашивающем подтверждение переименования класса формы и связанных с ней файлов, нужно согласиться.

В итоге должно получиться следующее - в окне "Обозреватель решений" форма и зависящие от неё файлы будут переименованы:

 

Изменяем свойства главной формы

Теперь в окне "Обозреватель решений" дважды кликнем на файл для формы FrmScrollbarExample.cs. В результате главная форма приложения будет выбрана в визуальном конструкторе, а справа-внизу появится список всех свойств формы.

Установим для формы следующие значения свойств:

Добавляем горизонтальный и вертикальный скроллбары на главную форму

Теперь нам нужно по очереди добавить горизонтальный скроллбар и вертикальный скроллбар на нашу форму.

Для этого слева в окне "Панель элементов" нужно найти нужные элементы, представляющие собой полосы прокрутки.

Выглядят они следующим образом:

Горизонтальный скроллбар представлен элементом управления HScrollBar:

 

Вертикальный скроллбар представлен элементом управления VScrollBar:

По очереди перетащим каждый из них на форму и установим следующие свойства:

Для горизонтального скроллбара:

Для вертикального скроллбара:

Теперь мы готовы к тому, чтобы написать код для нашей главной формы.

Пишем код для главной формы приложения

Дважды кликнем по главной форме из представления визуального конструктора. Откроется редактор кода для класса главной формы, в начало класса добавим константы, задающие количество клеток по горизонтали и вертикали, а также словарь colorMap, который будет содержать номера клеток в качестве ключей и конкретный цвет клетки в качестве значения для указанного ключа:

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

namespace ScrollbarExample {
    public partial class FrmScrollbarExample : Form {
        /// <summary>
        /// Количество клеток по горизонтали
        /// </summary>
        const int MAX_CELLS_HORIZONTAL = 80;

        /// <summary>
        /// Количество клеток по вертикали
        /// </summary>
        const int MAX_CELLS_VERTICAL = 50;

        private Dictionary<int, Color> colorMap = new Dictionary<int, Color>();

        /// ... остальной код формы
}

Теперь напишем следующий код для события загрузки главной формы (метод FrmScrollbarExample_Load уже должен существовать в редакторе после двойного клика на форме, поэтому в него нужно добавить лишь три строки кода):

        /// <summary>
        /// Загрузка главной формы приложения
        /// </summary>
        /// <param name="sender">объект главной формы, сгенерировавший событие</param>
        /// <param name="e">параметры события</param>
        private void FrmScrollbarExample_Load(object sender, EventArgs e) {
            DoubleBuffered = true;
            hScrollBar1.Maximum = MAX_CELLS_HORIZONTAL * 50 - this.Width + 50;
            vScrollBar1.Maximum = MAX_CELLS_VERTICAL * 50 - this.Height + 80;
        }

Разберём три строки, что добавили в метод загрузки формы: DoubleBuffered = true устанавливает в значение true свойство DoubleBuffered для главной формы - это позволяет главной форме использовать двойную буферизацию, за счёт чего отрисовка всех клеток на форме не будет мерцать, а будет плавной. В двух других строках мы устанавливаем свойство Maximum для горизонтальной и вертикальной полос прокрутки, вычисляя их как произведение максимального числа клеток по горизонтали/вертикали на размер клетки в пикселях (50), затем вычитаем ширину/высоту самой формы и прибавляем числа 50 и 80 для горизонтального и вертикального скроллбара, соответственно. Числа 50 и 80 были подобраны экспериментально, они позволяют увидеть целиком ряд последних клеток по горизонтали и вертикали, когда позиция бегунка скроллбара будет максимальной. При желании вы можете увеличить эти числа и посмотреть на результат - вы просто будете видеть больше серого фона самой формы.

Теперь добавим в код формы следующий метод GetCellColor:

        /// <summary>
        /// Получает цвет для очередной клетки
        /// </summary>
        /// <param name="cellNumber">текущий номер клетки</param>
        /// <param name="random"></param>
        /// <returns>Возвращает цвет для клетки поля с заданным номером</returns>
        private Color GetCellColor(int cellNumber, Random random) {            
            Color cellColor;
            if (colorMap.ContainsKey(cellNumber)) {
                cellColor = colorMap[cellNumber];
            } else {
                int red = random.Next(128, 256);
                int green = random.Next(128, 256);
                int blue = random.Next(128, 256);

                cellColor = Color.FromArgb(red, green, blue);
                colorMap.Add(cellNumber, cellColor);
            }
            return cellColor;
        }

Метод будет получать цвет конкретной клетки поля по её номеру. В качестве второго аргумента для метода мы будем впоследствии передавать заранее созданный экземпляр класса Random, который умеет генерировать случайные числа в заданном диапазоне.

Логика метода довольно проста: сначала мы проверяем, есть ли уже в словаре colorMap ключ с номером клетки cellNumber. Если да, то просто возвращаем заранее сохранённый в словаре цвет клетки по ключу. Если же в словаре ещё нет ключа с номером клетки cellNumber, то с помощью переданного в метод экземпляра random мы сгенерируем значение для красного, зелёного и синего цветов в диапазоне от 128 до 255 (граничное значение 256 не включено в диапазон генерируемых значений). Далее мы генерируем цвет для клетки поля и сохраняем его в словаре.

Метод всегда возвращает цвет клетки с заданным номером - независимо от того, был ли он уже сохранён в словаре или только что был добавлен в словарь.

Далее нам нужно будет написать метод с именем GetFontXDelta, который вычислит смещение шрифта внутри клетки при отрисовке номера клетки внутри её границ:

        /// <summary>
        /// Получить смещения шрифта по оси X. В зависимости от величины номера клетки
        /// нужно добавлять дополнительное смещение для центрирования текста с её номером внутри самой клетки
        /// </summary>
        /// <param name="figureNumber">номер клетки, для которого вычислить смещение шрифта по оси </param>
        /// <returns>значение смещения по оси X для шрифта с заданным номером клетки</returns>
        private int GetFontXDelta(int figureNumber) {
            if (figureNumber >= 1 && figureNumber < 10) {
                return 18;
            } else if (figureNumber >= 10 && figureNumber < 100) {
                return 14;
            } else if (figureNumber >= 100 && figureNumber < 1000) {
                return 8;
            } else {
                return 2;
            }
        }

Добавим его в код главной формы, его мы будем вызывать чуть позже - при отрисовке клеток на форме и обработке события Paint формы.

Теперь нам нужно сгенерировать два метода-обработчика для событий Paint и SizeChanged главной формы. Для этого вернёмся к визуальному конструктору формы и выделим саму форму. В нижнем правом окне "Свойства" нажмём на иконку молнии для просмотра всех событий формы и напротив событий Paint и SizeChanged сделаем двойной клик. Ниже показан пример, как это делается для событий Paint, для события SizeChanged всё делается аналогичным образом:

 

Теперь напишем следующий код для сгенерированных обработчиков:

        /// <summary>
        /// Отрисовка главной формы
        /// </summary>
        /// <param name="sender">объект главной формы, сгенерировавший событие</param>
        /// <param name="e">параметры события</param>
        private void FrmScrollbarExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            Font font = new Font("Tahoma", 14, FontStyle.Regular, GraphicsUnit.Point);
            Random random = new Random();

            Brush blackBrush = new SolidBrush(Color.Black);
            Pen blackPen = new Pen(blackBrush, 1);

            int cellNumber = 1;
            for (int nofFiguresVert = 1; nofFiguresVert <= MAX_CELLS_VERTICAL; nofFiguresVert++) {
                for (int nofFiguresHoriz = 1; nofFiguresHoriz <= MAX_CELLS_HORIZONTAL; nofFiguresHoriz++) {
                    Color cellColor = GetCellColor(cellNumber, random);
                    Brush fillBrush = new SolidBrush(cellColor);

                    int x = hScrollBar1.Visible ? (nofFiguresHoriz - 1) * 50 - hScrollBar1.Value : (nofFiguresHoriz - 1) * 50;
                    int y = vScrollBar1.Visible ? (nofFiguresVert - 1) * 50 - vScrollBar1.Value : (nofFiguresVert - 1) * 50;

                    Rectangle rect = new Rectangle(x, y, 50, 50);
                    Rectangle fillRect = new Rectangle(x + 1, y + 1, 49, 49);

                    g.DrawRectangle(blackPen, rect);
                    g.FillRectangle(fillBrush, fillRect);

                    Point pFont = new Point(x + GetFontXDelta(cellNumber), y + 15);

                    g.DrawString(cellNumber.ToString(), font, blackBrush, pFont);
                    cellNumber++;

                    fillBrush.Dispose();
                }
            }

            blackPen.Dispose();
            blackBrush.Dispose();
        }

        /// <summary>
        /// Обработка события изменения размера главной формы
        /// </summary>
        /// <param name="sender">объект главной формы, сгенерировавший событие</param>
        /// <param name="e">параметры события</param>
        private void FrmScrollbarExample_SizeChanged(object sender, EventArgs e) {
            int width = MAX_CELLS_HORIZONTAL * 50 - this.Width + 50;
            int height = MAX_CELLS_VERTICAL * 50 - this.Height + 80;

            if (width < 0) {
                hScrollBar1.Visible = false;
            } else {
                hScrollBar1.Visible = true;
                hScrollBar1.Maximum = width;
                hScrollBar1.Minimum = 0;
            }

            if (height < 0) {
                vScrollBar1.Visible = false;
            } else {
                vScrollBar1.Visible = true;
                vScrollBar1.Maximum = height;
                vScrollBar1.Value = 0;
            }

            Invalidate();
        }

Далее идут два раздела с описанием кода в этих методах-обработчиках. Читатели, которым код понятен и без пояснений, могут пропустить их.

Разбор метода-обработчика FrmScrollbarExample_Paint

В первой строке метода мы получаем в переменную g ссылку на объект класса Graphics, который уже присутствует в переменной e, представляющей собой параметры события Paint:

Graphics g = e.Graphics;

Это сделано просто для удобства, чтобы потом каждый раз для обращения к объекту класса Graphics нам не приходилось писать e.Graphics.

Далее мы создаём объект шрифта, которым будем "писать" внутри нарисованной клетки:

Font font = new Font("Tahoma", 14, FontStyle.Regular, GraphicsUnit.Point);

Следующей строкой создаём экземпляр класса Random, который мы будем использовать для генерации случайного цвета очередной клетки:

Random random = new Random();

Далее мы создаём сначала экземпляр класса SolidBrush и сохраняем его в переменную blackBrush. Он представляет собой сплошную "кисть" для рисования границ наших клеток, в аргументе для конструктора указываем, что цвет кисти - чёрный. Также создаём объект Pen (представляет собой "ручку") с заданными параметрами кисти и толщины штриха в 1 пиксель:

Brush blackBrush = new SolidBrush(Color.Black);
Pen blackPen = new Pen(blackBrush, 1);

Рисование границ клетки будет осуществляться с помощью экземпляра blackPen, объект кисти, как видим, нужен лишь для создания объекта класса Pen.

Далее, перед входом в циклы рисования поля из клеток мы инициализируем текущий номер клетки единицей:

int cellNumber = 1;

Затем мы организуем два цикла: внешний для отрисовки клеток по вертикали и вложенный в него цикл для отрисовки клеток по горизонтали:

for (int nofFiguresVert = 1; nofFiguresVert <= MAX_CELLS_VERTICAL; nofFiguresVert++) {
	for (int nofFiguresHoriz = 1; nofFiguresHoriz <= MAX_CELLS_HORIZONTAL; nofFiguresHoriz++) {
		// ... код внутри цикла ...

	}
}

Для каждой итерации вложенного цикла мы будем рисовать очередную клетку поля.

Разберём, что происходит во вложенном цикле.

Сначала мы получаем цвет клетки по её номеру - через вызов ранее созданного метода GetCellColor. И создаём кисть с полученным цветом для окрашивания содержимого клетки:

Color cellColor = GetCellColor(cellNumber, random);
Brush fillBrush = new SolidBrush(cellColor);

Теперь мы должны вычислить координаты для левого верхнего угла рисуемой нами клетки:

int x = hScrollBar1.Visible ? (nofFiguresHoriz - 1) * 50 - hScrollBar1.Value : (nofFiguresHoriz - 1) * 50;
int y = vScrollBar1.Visible ? (nofFiguresVert - 1) * 50 - vScrollBar1.Value : (nofFiguresVert - 1) * 50;

В случае, когда горизонтальный скроллбар видим, то мы учитываем текущую позицию его бегунка hScrollBar1.Value. Если же горизонтальный скроллбар скрыт, то мы считаем, что горизонтальные клетки полностью поместились на форме, поэтому просто используем формулу (nofFiguresHoriz - 1) * 50 для вычисления координаты x левого верхнего угла клетки. Почему именно эта формула? Потому что размерность наших клеток - 50x50 пикселей, и для переменной внешнего цикла nofFiguresHoriz, равной 1, мы получим (1 - 1) * 50, т.е. 0, значит самая первая клетка будет рисоваться в самом первом пикселе формы с индексом 0 по горизонтали. 

То же самое мы делаем и для координаты y, которая показывает текущее смещение по вертикали для левого верхнего угла рисуемой клетки.

Далее идёт код, который рисует внешнюю границу клетки, используя прямоугольник rect и осуществляет заполнение прямоугольной области fillRect клетки заданным цветом:

Rectangle rect = new Rectangle(x, y, 50, 50);
Rectangle fillRect = new Rectangle(x + 1, y + 1, 49, 49);

g.DrawRectangle(blackPen, rect);
g.FillRectangle(fillBrush, fillRect);

Наконец, вложенный цикл заканчивается следующими строками кода:

Point pFont = new Point(x + GetFontXDelta(cellNumber), y + 15);

g.DrawString(cellNumber.ToString(), font, blackBrush, pFont);
cellNumber++;

fillBrush.Dispose();

Здесь мы в переменную pFont сохраняем координаты той точки, в которой должен быть отрисован текст, содержащий номер самой клетки. По оси X нам требуется дополнительное смещение, поскольку числа от 0 до 9 имеют одну "ширину", числа от 10 до 99 - другую ширину и так далее. Для того, чтобы номер клетки всегда был отрисован по центру клетки нам и нужен метод GetFontXDelta, который вернёт нужную нам величину смещения шрифта, которую мы прибавляем к координате x.

Для рисования текста используется доступный метод DrawString в классе Graphics. Как видно, в него передаётся сама строка с номером клетки, шрифт, которым необходимо нарисовать надпись в клетке, объект кисти и, наконец, точку pFont для левого верхнего угла области отрисовки текста.

После отрисовки надписи с номером клетки мы увеличиваем номер текущей клетки на единицу и освобождаем ресурсы, занимаемые кистью fillBrush, поскольку её мы создаём каждый раз во вложенном цикле.

Метод завершается двумя строками, освобождающими ресурсы, выделенные под объекты классов SolidBrush и Pen, созданные до входа в циклы:

blackPen.Dispose();
blackBrush.Dispose();

Разбор метода-обработчика FrmScrollbarExample_SizeChanged

Метод-обработчик вызывается при изменениях размера главной формы. Его предназначение в том, чтобы отрегулировать состояние наших полос прокрутки в зависимости от текущих размеров формы.

Поэтому сначала мы вычисляем ширину (width) и высоту (height), которые затем должны стать максимальным значением горизонтального и вертикального скроллбара - но в том случае, если они неотрицательны. Ведь может случиться и так, что ширина/высота формы (к примеру, в развёрнутом виде) превысит произведения MAX_CELLS_HORIZONTAL * 50 и MAX_CELLS_VERTICAL * 50.

int width = MAX_CELLS_HORIZONTAL * 50 - this.Width + 50;
int height = MAX_CELLS_VERTICAL * 50 - this.Height + 80;

Следующим шагом мы проверяем - отрицательны или нет получившиеся значения. Если отрицательны, то мы скрываем конкретный скроллбар (признак того, что все клетки поля видимы). Если неотрицательны - устанавливаем свойства Maximum и Minimum для конкретного скроллбара и делаем его видимым:

if (width < 0) {
	hScrollBar1.Visible = false;
} else {
	hScrollBar1.Visible = true;
	hScrollBar1.Maximum = width;
	hScrollBar1.Minimum = 0;
}

if (height < 0) {
	vScrollBar1.Visible = false;
} else {
	vScrollBar1.Visible = true;
	vScrollBar1.Maximum = height;
	vScrollBar1.Value = 0;
}

Последней строкой метода мы вызываем метод Invalidate() - для того, чтобы принудительно сделать недействительной всю поверхность главной формы и вызвать её перерисовку:

Invalidate();

Заключительная часть - программируем реакцию скроллбаров на изменение позиции бегунка

Наше приложение почти полностью готово. Всё, что нам осталось сделать - это вернуться на главную форму и для каждого из скроллбаров сгенерировать их методы-обработчики для события Scroll. Это событие отвечает за "скроллинг", т.е. прокрутку бегунка для конкретного скроллбара. Сгенерировать методы можно также через двойной клик напротив имени события, как мы уже делали выше для Paint и SizeChanged. После генерации методов-обработчиков просто вызываем Invalidate() из каждого из них, это заставит главную форму перерисоваться в момент "скроллинга":

        /// <summary>
        /// Изменение позиции бегунка для горизонтального скроллбара
        /// </summary>
        /// <param name="sender">объект горизонтального скроллбара, сгенерировавший событие</param>
        /// <param name="e">параметры события</param>
        private void hScrollBar1_Scroll(object sender, ScrollEventArgs e) {
            Invalidate();
        }

        /// <summary>
        /// Изменение позиции бегунка для вертикального скроллбара
        /// </summary>
        /// <param name="sender">объект вертикального скроллбара, сгенерировавший событие</param>
        /// <param name="e">параметры события</param>
        private void vScrollBar1_Scroll(object sender, ScrollEventArgs e) {
            Invalidate();
        }

На этом всё, приложение готово, и вы можете запустить его и протестировать самостоятельно работу горизонтальной и вертикальной полос прокрутки, а также посмотреть на реакцию формы на события прокрутки бегунков для скроллбаров.

Пишите свои вопросы и комментарии под статьей, буду рад обратной связи.

Ссылка на готовый пример для среды разработки Microsoft Visual Studio, рассмотренный в этой статье:

https://allineed.ru/our-products/download/4-allineed-ru-examples/17-csharp-demo-working-with-scrollbars