Новые технологии настолько вошли в нашу жизнь, что даже у пятилетнего ребёнка есть мощный смартфон или (и) планшет. Поэтому открылась новая ниша «Игры для детей». Конечно же, я взял эту тему для примера работы с SQLite.
Upd (20.04.14). Проверил код на Delphi XE6 и добавил исходники для новой версии IDE.
Upd (20.04.14). Проверил код на Delphi XE6 и добавил исходники для новой версии IDE.
Не будем создавать кучу форм для отображения разной информации. Вместо этого создадим на форме один TTabControl с нужным количеством вкладок.
Для начала определяем вкладки:
- Меню
- Играть
- Статистика
- Правила
0. Основа.
В приложении будет 4 окошка. Для их реализации воспользуемся TTabControl, добавив в него 4 вкладки. Приложение разрабатывается для мобильных девайсов (телефонов) с размером экрана не менее 3.2 дюйма и не более 4.5 дюйма, хотя работать будет и на планшетах тоже.
1. Создаём окно с меню.
Нам понадобится:
- Два Label – для вывода названия приложения и вывода поздравления
- Два TLayout – для позиционирования элементов относительно формы
- Четыре кнопки TSpeedButton – для создания меню, каждая кнопка ссылается на определённую вкладку TTabControl
- Один TActionList – для перемещения между вкладками TTabControl
Структура на стадии «Создание меню»:
2. Вкладка «Играть» - Создание.
Этот пункт самый большой, т.к. мы создадим файл базы, заполним его, подключим к проекту. Решим, какие элементы нужны на форме, и как лучше расположить их. Загрузим картинки в ресурсы приложения. Напишем код для работы с файлом базы, добавим элементы для работы с базой на форму.
2.1. Начнём с создания файла базы, заполнения и подключения к проекту
В приложении будем использовать локалькую базу данных(SQLite). Файл назовём «rebusy.db». Сами ребусы будут в виде картинок (хранятся в ресурсах), а подсказки и ответы будут в текстовом виде храниться в базе.
Структура таблицы «list_rebusy»:
CREATE TABLE list_rebusy (
Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
answer TEXT,
hint TEXT,
resolved INTEGER DEFAULT ( 0 )
);
Т.к. картинки и ответы на ребусы связаны, то идентификатор (id, начинается с единицы) из базы будет браться в качестве имени картинки. Формат картинок «.png». Как вы наверно уже догадались, самое нудное – это заполнить базу, поэтому я не стал добавлять в неё 100-500 записей. В базе 19 записей.
Примечание: Все ребусы (картинки, подсказки, ответы) взяты из интернета, я ни чего не изменял. Базу создавал в программе «SQLiteStudio».
После создания базы её необходимо подключить к проекту:
Незабываем задеплоить файл базы в папку «.\assets\internal\».
2.2. Решаем, какие элементы нужны нам на форме.
На форме, во вкладке «Play Game» нам понадобятся:
- Один TToolBar1, на него положим одну кнопку TSpeedButton (возвращаемся в меню) и один TLabel (будем выводить номер ребуса)
- Один TLayout, на него кладём один TImage – в него будем подгружать картинку из ресурсов
- Один TLabel – выводим подсказку
- Один TLayout, на него положим один TClearingEdit, две кнопки TSpeedButton (Проверить и Правила)
2.3. Загружаем картинки в ресурсы приложения.
Картинок у меня 19 штук (общий вес 234 КБ).
Хранить их можно где угодно:
- В ресурсах приложения «Resources and Images…»
- В файле базы данных, используя BLOB
- В папке с проектом, т.е. задеплоить все картинки
- В архиве, который лежит в папке с проектом
Я решил, что в этом примере, буду хранить картинки, используя первый пункт.
Добавляем картинки:
Примечание: Если вы разработчик Firemonkey, то прошу вас: Сделайте, пожалуйста, человеческую сортировку в этом окне и чтобы сохранялись размеры/положение этого окна (если мы их изменили) при следующем открытии.
2.3. Пишем код для работы с базой
Нам необходимо читать из базы по одной записи (рандомно). Забегая вперёд: ещё будет необходимо собирать/сбрасывать статистику. Но это всё впереди, на данном этапе нам необходимо выводить по одной записи и обновлять запись в случае совпадения ответа.
Чтобы наше приложение умело подключаться к базе, необходимо перейти в «Data Explorer», зажать левую кнопку мышки на базе и перетащить её на форму. На форме появится компонент «TSQLConnection».
Остаётся добавить на форму компонент «TSQLQuery» со свойствами:
- Name: SQLQSelectRand
- SQLConnection: RebusyDB
- SQL: «SELECT * FROM `list_rebusy` WHERE `resolved`='0' ORDER BY RANDOM() LIMIT 1» (без начальной и последней кавычек) – рандомно извлекаем одну запись
- Name: SQLQUpdate
- Params:
- Id - DataType: ftInteger; Name: id; ParamType: ptInput
- SQL: «UPDATE `list_rebusy` SET `resolved`='1' WHERE `id`= (:id)»
- SQLConnection: RebusyDB
Для извлечения создадим отдельную процедуру «SQLSelect», в ней будем проверять, остались ли ещё не разгаданные ребусы и если они остались, то выводим ребус, если не остались то выводим поздравление.
Как это выглядит:
Структура вкладки:
3. Вкладка «Статистика»
На форме, во вкладке «Statistics» нам понадобятся:
- Один TToolBar1, на него положим одну кнопку TSpeedButton (возвращаемся в меню) и один TLabel
- Три Label
- Одну кнопку TSpeedButton, для сброса статистики
- Name: SQLQCount
- SQLConnection: RebusyDB
- SQL: «SELECT COUNT ( * ) FROM `list_rebusy` UNION SELECT COUNT ( * ) FROM `list_rebusy` WHERE `resolved` = '1'»
- Name: SQLQClear
- SQL: « UPDATE `list_rebusy` SET `resolved`='0' »
- SQLConnection: RebusyDB
Структура:
Теперь код:
Для подсчёта статистики используем процедуру «SQLCount».
4. Вкладка «Правила»
Тут всё просто, я скопировал правила разгадывания ребусов с первого попавшегося сайта.
Скриншоты приложения:
Код приложения:
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.TabControl,
FMX.Layouts, FMX.StdCtrls, System.Actions, FMX.ActnList, FMX.Objects, FMX.Edit,
Data.DbxSqlite, Data.FMTBcd, Data.DB, Data.SqlExpr;
type
TForm1 = class(TForm)
VertScrollBox1: TVertScrollBox;
TabControl1: TTabControl;
tiMenu: TTabItem;
tiPlayGame: TTabItem;
tiRules: TTabItem;
tiStatistics: TTabItem;
lAppName: TLabel;
Layout1: TLayout;
Layout2: TLayout;
sbPlay: TSpeedButton;
sbStatistics: TSpeedButton;
sbRules: TSpeedButton;
sbExit: TSpeedButton;
ActionList1: TActionList;
ChangeTabPlayGame: TChangeTabAction;
ChangeTabStatistics: TChangeTabAction;
ChangeTabRules: TChangeTabAction;
ToolBar1: TToolBar;
sbMenu: TSpeedButton;
lRebusNum: TLabel;
ChangeTabMenu: TChangeTabAction;
Image1: TImage;
Layout3: TLayout;
sbCheck: TSpeedButton;
sbRules2: TSpeedButton;
ceAnswer: TClearingEdit;
RebusyDB: TSQLConnection;
SQLQSelectRand: TSQLQuery;
lHint: TLabel;
Layout4: TLayout;
SQLQUpdate: TSQLQuery;
lResultWin: TLabel;
ToolBar2: TToolBar;
sbMenu2: TSpeedButton;
lStatistics: TLabel;
lCountAll: TLabel;
lCountResolved: TLabel;
lCountNotSolved: TLabel;
SQLQCount: TSQLQuery;
sbClearStatistics: TSpeedButton;
SQLQClear: TSQLQuery;
ToolBar3: TToolBar;
sbMenu3: TSpeedButton;
lRules: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
Layout5: TLayout;
Image2: TImage;
ScrollBox1: TScrollBox;
Layout6: TLayout;
Image3: TImage;
Layout7: TLayout;
Image4: TImage;
Layout8: TLayout;
Image5: TImage;
Layout9: TLayout;
Image6: TImage;
procedure sbCheckClick(Sender: TObject);
procedure RebusyDBBeforeConnect(Sender: TObject);
procedure sbExitClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Image1Click(Sender: TObject);
procedure sbClearStatisticsClick(Sender: TObject);
procedure sbPlayClick(Sender: TObject);
procedure FormFocusChanged(Sender: TObject);
procedure FormVirtualKeyboardHidden(Sender: TObject;
KeyboardVisible: Boolean; const Bounds: TRect);
procedure FormVirtualKeyboardShown(Sender: TObject;
KeyboardVisible: Boolean; const Bounds: TRect);
procedure FormKeyUp(Sender: TObject; var Key: Word; var KeyChar: Char;
Shift: TShiftState);
private
{ Private declarations }
FKBBounds: TRectF;
FNeedOffset: Boolean;
IdSql: integer; // идентификатор ребуса
CorrectAnswer, Hint: string;
ImageWidthDef, ImageHeightDef: single;
procedure CalcContentBoundsProc(Sender: TObject;
var ContentBounds: TRectF);
procedure RestorePosition;
procedure UpdateKBBounds;
public
{ Public declarations }
procedure SQLSelect;
procedure SQLCount;
end;
var
Form1: TForm1;
implementation
{$R *.fmx}
uses
System.IOUtils, System.Math, FMX.Platform.Android;
procedure TForm1.CalcContentBoundsProc(Sender: TObject;
var ContentBounds: TRectF);
begin
if FNeedOffset and (FKBBounds.Top > 0) then
begin
ContentBounds.Bottom := Max(ContentBounds.Bottom,
2 * ClientHeight - FKBBounds.Top);
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
TabControl1.ActiveTab := tiMenu;
VertScrollBox1.OnCalcContentBounds := CalcContentBoundsProc;
// Сохраняем размеры Image1
ImageWidthDef := Image1.Width;
ImageHeightDef := Image1.Height;
RebusyDB.Connected := True;
// Get Rebus
SQLSelect;
// Get Statistics
SQLCount;
end;
procedure TForm1.FormFocusChanged(Sender: TObject);
begin
UpdateKBBounds;
end;
procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; var KeyChar: Char;
Shift: TShiftState);
begin
if ceAnswer.IsFocused then
begin
if Key = 13 then
sbCheckClick(Self);
end
else
begin
if (Key = vkHardwareBack) AND (TabControl1.ActiveTab <> tiMenu) then
begin
TabControl1.ActiveTab := tiMenu;
Key := 0;
end;
end;
end;
procedure TForm1.FormVirtualKeyboardHidden(Sender: TObject;
KeyboardVisible: Boolean; const Bounds: TRect);
begin
FKBBounds.Create(0, 0, 0, 0);
FNeedOffset := False;
RestorePosition;
end;
procedure TForm1.FormVirtualKeyboardShown(Sender: TObject;
KeyboardVisible: Boolean; const Bounds: TRect);
begin
FKBBounds := TRectF.Create(Bounds);
FKBBounds.TopLeft := ScreenToClient(FKBBounds.TopLeft);
FKBBounds.BottomRight := ScreenToClient(FKBBounds.BottomRight);
UpdateKBBounds;
end;
procedure TForm1.Image1Click(Sender: TObject);
begin
lHint.Visible := True;
end;
procedure TForm1.RebusyDBBeforeConnect(Sender: TObject);
begin
RebusyDB.Params.Values['Database'] := TPath.Combine(TPath.GetDocumentsPath, 'rebusy.db');
end;
procedure TForm1.RestorePosition;
begin
VertScrollBox1.ViewportPosition := PointF(VertScrollBox1.ViewportPosition.X, 0);
TabControl1.Align := TAlignLayout.alClient;
VertScrollBox1.RealignContent;
end;
procedure TForm1.sbCheckClick(Sender: TObject);
var
UserAnswer: string;
begin
UserAnswer := AnsiLowerCase(Trim(ceAnswer.Text));
if (UserAnswer <> '') AND (UserAnswer = CorrectAnswer) then
begin
showmessage('Это правильный ответ!');
try
lHint.Visible := False;
ceAnswer.Text := '';
// Update rebus
SQLQUpdate.ParamByName('id').AsInteger := IdSql;
SQLQUpdate.ExecSQL();
// Select new rebus
SQLSelect;
// Update Statistics
SQLCount;
except
on e: Exception do
begin
ShowMessage(e.Message);
end;
end;
end
else
ShowMessage('Это НЕ правильный ответ!');
end;
procedure TForm1.sbClearStatisticsClick(Sender: TObject);
begin
try
SQLQClear.ExecSQL();
// Select new rebus
SQLSelect;
// Update Statistics
SQLCount;
except
on e: Exception do
begin
ShowMessage(e.Message);
end;
end;
end;
procedure TForm1.sbExitClick(Sender: TObject);
begin
RebusyDB.Connected := False;
MainActivity.finish;
end;
procedure TForm1.sbPlayClick(Sender: TObject);
begin
TabControl1.ActiveTab := tiPlayGame;
//ChangeTabPlayGame.Tab := tiPlayGame;
//ChangeTabPlayGame.Execute;
end;
procedure TForm1.SQLCount;
var
CountResolved, CountAll: integer;
begin
try
// Statistics
SQLQCount.Active := True;
SQLQCount.Open;
SQLQCount.First;
CountResolved := SQLQCount.Fields.Fields[0].AsInteger;
SQLQCount.Next;
if SQLQCount.Fields.Fields[0].AsInteger <> 0 then
CountAll := SQLQCount.Fields.Fields[0].AsInteger
else
CountAll := CountResolved;
lCountAll.Text := 'Всего ребусов: ' + CountAll.ToString;
lCountResolved.Text := 'Разгадано: ' + CountResolved.ToString;
lCountNotSolved.Text := 'Осталось: ' + IntToStr(CountAll-CountResolved);
SQLQCount.Close;
SQLQCount.Active := False;
except
on e: Exception do
begin
ShowMessage(e.Message);
end;
end;
end;
procedure TForm1.SQLSelect;
var
InStream: TResourceStream;
ImageRatio, BitmapRatio: single; // Соотношение сторон Image1 и Bitmap
Ratio, MaxImageWidth, MaxImageHeight: Single; // Соотношение; Возможная Ширина; Возможная Высота
begin
try
SQLQSelectRand.Active := True;
SQLQSelectRand.Open;
if not SQLQSelectRand.IsEmpty then
begin
sbPlay.Enabled := True;
IdSql := SQLQSelectRand.FieldByName('id').AsInteger;
CorrectAnswer := AnsiLowerCase(Trim(SQLQSelectRand.FieldByName('answer').AsString));
Hint := SQLQSelectRand.FieldByName('hint').AsString;
lRebusNum.Text := 'Ребус № ' + IntToStr(IdSql);
lHint.Text := 'Подсказка: ' + hint;
// Load image
InStream := TResourceStream.Create(HInstance, 'PngImage_' + IntToStr(IdSql), RT_RCDATA);
try
Image1.Bitmap.LoadFromStream(InStream);
finally
InStream.Free;
end;
Image1.Width := ImageWidthDef;
Image1.Height := ImageHeightDef;
ImageRatio := Image1.Width/Image1.Height;
BitmapRatio := Image1.Bitmap.Width/Image1.Bitmap.Height;
if ImageRatio > BitmapRatio then
begin
Ratio := BitmapRatio;
MaxImageWidth := Image1.Height * Ratio;
if MaxImageWidth > Image1.Width then
Image1.Width := Image1.Height * (ImageRatio - (Ratio - ImageRatio))
else
Image1.Width := MaxImageWidth;
end
else
begin
if ImageRatio <= BitmapRatio then
begin
Ratio := Image1.Bitmap.Height/Image1.Bitmap.Width;
MaxImageHeight := Image1.Width * Ratio;
if MaxImageHeight > Image1.Height then
Image1.Height := Image1.Width * (Image1.Height/Image1.Width) - (Ratio - (Image1.Height/Image1.Width))
else
Image1.Height := MaxImageHeight;
end;
end;
end
else
begin
lResultWin.Text := 'Поздравляем!' + #13#10 + 'Вы разгадали все ребусы!';
lResultWin.Visible := True;
TabControl1.ActiveTab := tiMenu;
sbPlay.Enabled := False;
end;
SQLQSelectRand.Close;
SQLQSelectRand.Active := False;
except
on e: Exception do
begin
ShowMessage(e.Message);
end;
end;
end;
procedure TForm1.UpdateKBBounds;
var
LFocused : TControl;
LFocusRect: TRectF;
begin
FNeedOffset := False;
if Assigned(Focused) then
begin
LFocused := TControl(Focused.GetObject);
LFocusRect := LFocused.AbsoluteRect;
LFocusRect.Offset(VertScrollBox1.ViewportPosition);
if (LFocusRect.IntersectsWith(TRectF.Create(FKBBounds))) and
(LFocusRect.Bottom > FKBBounds.Top) then
begin
FNeedOffset := True;
TabControl1.Align := TAlignLayout.alHorizontal;
VertScrollBox1.RealignContent;
Application.ProcessMessages;
VertScrollBox1.ViewportPosition :=
PointF(VertScrollBox1.ViewportPosition.X,
LFocusRect.Bottom - FKBBounds.Top);
end;
end;
if not FNeedOffset then
RestorePosition;
end;
end.
Видео:
Исходный код (Delphi XE5 Update 2): Скачать с Google Drive
Исходный код (Delphi XE6): Скачать с Google Drive
Исходный код (Delphi XE6): Скачать с Google Drive
Спасибо за внимание.
p.s. Надеюсь, статья кому-то пригодится.
p.s.2. Благодарю Бровина Ярослава и Рощина Сергея, за советы по некоторым вопросам.




