БлогNot. Visual C++: делаем приложение на основе ListBox

Visual C++: делаем приложение на основе ListBox

Здесь я приведу пример на работу со списком общего назначения ListBox, а по ListView сделаю попозже отдельную заметку. Для QT5 чем-то похожие коды есть вот тут, а для Builder - здесь.

Как и у других классов библиотеки .NET, у списка ListBox есть масса свойств и методов, подробно они описаны, разве что, в справке msdn по ссылке выше, а в первую очередь кажутся полезными следующие:

  • MultiColumn - разрешает вывод содержимого в несколько колонок;
  • ColumnWidth - ширина колонки в пикселах при многоколоночном списке;
  • SelectionMode - сколько элементов можно выбирать за один раз;
  • SelectedIndex - индекс первого выбранного элемента или -1;
  • SelectedItem - выбранный элемент (обычно строка);
  • Count - количество элементов в списке;
  • Items - изменяемый контейнер с элементами:

String ^s=ListBox1->Items[i]->ToString(); 
 //получили i-ый элемент как строку

String ^is = this->ListBox1->Items[this->ListBox1->SelectedIndex]->ToString();
 //получили строку, по которой выполнен щелчок

Названия основных методов, как обычно, просты и наглядны, думаю, нет нужды их и пояснять:

Items->Add(), Items->Insert(), Items->Clear(), Items->Remove("строка"), Items->RemoveAt(номер)

и т.д.

Конечно, добраться до некоторых нужных значений бывает нелегко, например, назначить ширину в пикселах столбца списка, исходя из ширины последнего элемента, придётся аж вот как:

int w=(int)ListBox1->CreateGraphics()->MeasureString(
 ListBox1->Items[ListBox1->Count-1]->ToString(), ListBox1->Font).Width;
 ListBox1->ColumnWidth = w; //узнали ширину последнего элемента в пикселах

Зато прочитать строки списка из файла с дескриптором fp, открытого обычной функцией fopen, будет совсем просто:

char s[128];
//и далее в цикле:
fgets (s,128,fp);
String ss = gcnew String (s);
ListBox1->Items->Add (ss->SubString(0,ss->Length-1));

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

Создадим приложение Windows Forms с одной формой Form1 фиксированного размера, например, у неё могут быть следующие свойства: FormsBorder=FixedDialog, MaximizeBox=false, Size=590,470, StartPosition=CenterScreen, Text="Справочник".

На форме размещён listBox1, привязанный "якорями" ко всем её сторонам, кроме нижней (Anchor=Top,Left,Right), для списка также создано контекстное меню contextMenuStrip1 с пунктами Добавить и Удалить, свойство Sorted=true, то есть, строки будут автоматически сортироваться.

Остальные элементы размещены на Panel1 внизу, у которой включены все "якоря" (Anchor=Top,Bottom,Left,Right).

Поля textBox1, textBox2 - это ввод номера телефона и комментария (формат справочника сделаем простейшим), Label1, Label2 - метки с рисунками (установлено свойство Image), у них Size=90,90, плюс загружены рисунки того же размера. Пара кнопок будет служить для поиска записи и очистки полей ввода.

Вот что примерно вышло:

окно приложения Телефонный справочник
окно приложения Телефонный справочник

В пользовательской секции кода модуля Form1.h (после #pragma endregion) начнём писать нужные для приложения функции.

Логичней и проще всего, если наше приложение будет автоматически считывать файл с данными при загрузке и автоматически сохранять его при выходе. Хотелось бы также, чтобы отсутствие файла в текущей папке не смущало приложение и оно могло бы создать файл заново. Напишем метод LoadFromFile для чтения из файла с фиксированным именем data.txt, а метод SaveToFile - для записи. Из кода видно, что мы отделяем номер телефона от фамилии или имени просто символом табуляции \t.

private: void LoadFromFile (String ^File, ListBox^  listBox1) { //чтение из файла
 String ^d, ^b;
 listBox1->Items->Clear();
 try {
  b = System::IO::File::ReadAllText(File);
 } 
 catch (...) {
  System::IO::File::WriteAllText("base.txt","");
  return;
 }
 while (b->Length>0) {
  int i=b->IndexOf("\n");
  if (i<0) continue;
  d=b->Substring(0,i);
  listBox1->Items->Add(d);
  b=b->Substring(i+1,b->Length-d->Length-1);
 }
 this->listBox1->SelectedIndex = -1;
}

private: void SaveToFile (String ^File, ListBox^  listBox1) { //запись в файл
 String ^a,^b;
 int n=listBox1->Items->Count;
 System::IO::File::Delete(File);
 for (int i=0; i<n; i++) {
  a=listBox1->Items[i]->ToString();
  b+=a->Concat(a,"\n");
 }
 System::IO::File::AppendAllText(File,b);
}

Пункт Добавить контекстного меню, вызываемого нажатием правой кнопки мыши на списке, обработает и поместит в список данные, которые набраны в полях ввода. При этом он гибко позволит как редактировать имеющиеся данные (если в списке есть выбранный элемент), так и создать новый элемент. Повторное добавление одинаковых записей исключим, а действие метода будем дублировать нажатием клавиши Enter во втором поле ввода (имя). Заодно сделаем так, чтобы при нажатии Enter в первом поле ввода курсор автоматически переходил во второе, а после Enter во втором поле не только добавлялась или изменялась запись, но и очищались поля ввода с установкой курсора обратно на первое поле. Это облегчит массовый ввод записей. Лишних сообщений об ошибках плодить не будем, от пришедших из прошлого века назойливых модальных окон с кнопкой ОК вообще следует избавляться.

private: System::Void toolStripMenuItem1_Click(System::Object^  sender, System::EventArgs^  e) { //кнопка Добавить
 String ^r="";
 String ^s1=this->textBox1->Text->Trim();
 String ^s2=this->textBox2->Text->Trim();
 if (!(String::IsNullOrEmpty(s1) || String::IsNullOrEmpty(s2))) {
  r=r->Concat(s1,"\t",s2);
  if (this->listBox1->SelectedIndex!=-1) {
   this->listBox1->Items[this->listBox1->SelectedIndex]=r;
   this->textBox1->Clear();
   this->textBox2->Clear();
   this->listBox1->SelectedIndex=-1;
  }
  else { 
   if (listBox1->FindString(r)==-1) this->listBox1->Items->Add(r); //не добавлять, если уже есть в списке
  }
 }
}

private: System::Void textBox1_KeyDown(System::Object^  sender, System::Windows::Forms::KeyEventArgs^  e) {
 //Обработка Enter в первом поле ввода
 if (e->KeyCode == Keys::Enter) this->textBox2->Focus();
}

private: System::Void textBox2_KeyDown(System::Object^  sender, System::Windows::Forms::KeyEventArgs^  e) {
 //Обработка Enter во втором поле ввода
 if (e->KeyCode == Keys::Enter) {
  this->toolStripMenuItem1_Click(this,e);
  this->button2_Click(this,e);
 }
}

Первому полю ввода также понадобится обработчик события KeyPress, разрешим там только цифры, пробел, Enter, Backspace и "минус" (дефис) с "плюсом". Заметим, что это не страхует нас от появления недопустимых символов, например, из Буфера Обмена. Можно придумать много способов контролировать это, например, сделать полю своё контекстное меню вместо системного и контролировать действия Вырезать-Копировать-Вставить, но самый простой путь - обработать событие Validating от поля и "не выпускать" из него, если что-то введено неверно. Вопросы корректности формата вводимых данных (в каких позициях разрешены дефисы и "плюс", а в каких цифры) оставим на совести пользователя, а вообще-то, для этого есть компонент MaskedTextBox и проверка регулярными выражениями.

private: System::Void textBox1_KeyDown(System::Object^  sender, System::Windows::Forms::KeyEventArgs^  e) {
 //Обработка Enter в первом поле ввода
 if (e->KeyCode == Keys::Enter) this->textBox2->Focus();
}

private: System::Void textBox2_KeyDown(System::Object^  sender, System::Windows::Forms::KeyEventArgs^  e) {
 //Обработка Enter во втором поле ввода
 if (e->KeyCode == Keys::Enter) {
  this->toolStripMenuItem1_Click(this,e);
  this->button2_Click(this,e);
 }
}

private: System::Void textBox1_KeyPress(System::Object^  sender, System::Windows::Forms::KeyPressEventArgs^  e) {
 //обработка нажатий клавиш в первом поле ввода
 char c=(char)e->KeyChar;
 if (Char::IsDigit(c) || c== (char)Keys::Back || c==(char)Keys::Enter || c== (char)'-' || c== (char)'+') return;
 else e->Handled = true;
}

private: System::Void textBox1_Validating(System::Object^  sender, System::ComponentModel::CancelEventArgs^  e) {
 //валидация введённой строки в певом поле ввода
 String ^t = "0123456789-+ ";
 for (int i=0; i<textBox1->Text->Length; i++) {
  String ^s = textBox1->Text->Substring(i,1);
  if (t->IndexOf(s)==-1) {
   e->Cancel=true; //отменить выход из поля, если недопустимые символы
   textBox1->SelectAll();
   return;
  }
 }
}

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

private: System::Void Form1_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e) {
 SaveToFile ("base.txt",this->listBox1);
}

private: System::Void Form1_Load(System::Object^  sender, System::EventArgs^  e) {
 LoadFromFile ("base.txt",this->listBox1);
}

Пункт меню Удалить сделает свою работу, если он вызван при каком-то выбранном в списке элементе:

private: System::Void toolStripMenuItem2_Click(System::Object^  sender, System::EventArgs^  e) { //кнопка Удалить
 if (this->listBox1->SelectedIndex!=-1) {
  this->listBox1->Items->Remove(this->listBox1->SelectedItem);
  if (this->listBox1->Items->Count==0) {
   this->listBox1->SelectedIndex = -1;
  }
 }
}

Обычный клик левой кнопкой по элементу списка разберёт хранящуюся там строку на части "номер" и "имя" и скопирует их в нижние поля ввода для редактирования.

private: System::Void listBox1_Click(System::Object^  sender, System::EventArgs^  e) { //клик по списку
 if (this->listBox1->SelectedIndex<0) return;
 String ^ s = this->listBox1->Items[this->listBox1->SelectedIndex]->ToString();
 int i=s->IndexOf("\t");
 if (i<0) return;
 String ^s1=s->Substring(0,i);
 this->textBox1->Text = s1;
 this->textBox2->Text = s->Substring(i+1,s->Length-s1->Length-1);
}

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

private: System::Void button1_Click(System::Object^  sender, System::EventArgs^  e) { //кнопка Найти
 String ^a;
 String ^s1=this->textBox1->Text->Trim();
 String ^s2=this->textBox2->Text->Trim();
 int n=listBox1->Items->Count;
 for (int i=0; i<n; i++) listBox1->SetSelected(i,false); //снять все выделения в списке
 for (int i=0; i<n; i++) {
  a=listBox1->Items[i]->ToString();
  int p=a->IndexOf("\t");
  String ^phone, ^name;
  if (p>-1) {
   phone=a->Substring(0,p);
   name=a->Substring(p);
   if (!String::IsNullOrEmpty(s1) && phone->IndexOf(s1)>-1 ||
       !String::IsNullOrEmpty(s2) && name->IndexOf(s2)>-1) {
    listBox1->SetSelected(i,true);
   }
  }
 }
}

Наконец, кнопка Очистить точно не представит проблемы. Она сотрёт содержимое нижних полей ввода и установит курсор в первое из них.

private: System::Void button2_Click(System::Object^  sender, System::EventArgs^  e) {
 this->textBox1->Clear();
 this->textBox2->Clear();
 this->textBox1->Focus();
}

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

 Скачать этот проект Visual C++ в архиве .zip (29 Кб)

26.09.2015, 12:40 [19864 просмотра]


теги: список программирование c++/cli

К этой статье пока нет комментариев, Ваш будет первым