15 | 12 | 2017

 Глава 1 Стратегия проектирования приложений

Система FoxPro 2.0 предлагает совершенно новый стиль программирования. Он также обеспечивает поддержку методов разработки экранов и меню, использованных в тысячах продуктов поддержки баз данных. 

Конструктор экранов (Screen Builder) побуждает программистов разрабатывать экраны определенного вида и функциональности. Хотя мне и нравится этот стандарт, я не хочу, чтобы у вас создалось впечатление, будто это единственный тип экрана, который возможно создать в FoxPro. Поэтому я начну с концепций, а средства разработчика оставлю на потом. Если вы будете рассматривать конструктор экранов скорее как инструмент, нежели как набор альтернатив, то будете чувствовать себя более комфортно и сможете сделать то, что вам нужно.

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

Простые модели

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

Плоский файл с простым меню

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


В конце программы располагается примерно 10 строк кода, где я использовал старомодный подход для получения значения переменной choice. В версии 2.0 вы можете использовать иной подход:

@ 24,0 CET nChoice PICTURE "* Accept; Ignore"

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

Плоский файл с выпадающими меню

Диалог Locate использует функцию GETEXPR() языка FoxPro, удобную для пользователей- программистов. Обратите внимание, что опция Continue становится активной только после выполнения команды Locate, когда та еще не исчерпала диапазон поиска. Код приложения, управляемого выпадающими меню, снабженного некоторыми дополнениями приведен в листинге 1-2. Эту и предыдущую программы объединяет простота написания и использования. При этом они имеют ряд раздражающих пользователей ограничений. Не всегда видно в каком поле расположен курсор. Типы проверок, которые вы можете организовать с экраном такого типа, ограничены. По этой причине программисты, писавшие на dBASE II, изобрели цикл обработки команды READ (READ loop).

Особые случаи: циклы обработки команды

До появления FoxPro 2.0 единственным способом создать сложные экраны было использование READ loop, названного так, потому что программа включала цикл, внутри которого отслеживалось, какое поле ввода должно быть активизировано следующим. С появлением версии 2.0 число причин использовать такую технику уменьшилось, но некоторое количество все же осталось. Особенно для случая, когда вы хотите дать пользователю возможность свободного перемещения между полями без использования мыши. На основании значения, возвращаемого LASTKEY(), вы можете манипулировать значением переменной _curobj, однако обеспечить быстрый переход к конкретному полю не представляется возможным. По сравнению с простотой программирования под FoxPro 2.0 и использованием мыши, затраты слишком велики. Каждый делает то, что он вынужден делать. Эта методика интересна хотя бы как исторический экскурс и как пример того, насколько стала проще жизнь с появлением READ CYCLE.

SuperGET

Наиболее простым вариантом цикла READ является так называемый цикл SuperGet, использующий структуру DO CASE для определения следующего активизируемого поля. Листинг 1-3 содержит код демонстрирует соответствующий экран. Предлагаемая программа обеспечивает выделение только активного поля. Кроме того, здесь легко организовать проверки ввода.

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

Это будет работать лучше

Если вы построите цикл на использовании массивов вместо структуры DO CASE, производительность будет намного выше. Мне доводилось видеть четырехкратное увеличение скорости при использовании массивов. Программы приведены в листинге 1-4 и листинге 1-5.

Это единственный способ обеспечить свободное перемещение по всему экрану без мыши, с использованием только клавиатуры (a lа Lotus 1-2-3).

Вариации на заданную тему

Рассматриваемая методика имеет значительные возможности расширения. Программа в листинге 1-6 предлагает несколько типов проверок, основанных на перехвате нажимаемых пользователем клавиш.

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

Кроме того, нажатие клавиши Home приводит к переводу курсора в первое поле, а клавиши End - в последнее.

И, наконец, я устроил так, что каждой функциональной клавише назначена строка из двух символов: символ CHR(29) и цифра. Участок кода:

SET CONSOLE OFF ACCEPT TO SecondByte SET CONSOLE ON

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

Способ использования циклов READ в новом смелом мире

Версия FoxPro 2.0 позволяет назначать функциональные и другие клавиши как "Hot keys", что делает приведенный выше маленький трюк не столь актуальным. Кроме того, использование READ...COLOR позволяет принудительно выделять цветом активное поле ввода. Помимо этого структура VALID теперь способна поддерживать до четырех уровней вложенности READ, а значит появились другие способы поддержки процедур анализа ввода, добавляющих новые процедуры анализа по ходу работы.

Ну и зачем тогда, можете вы спросить, заниматься всеми этими премудростями? Вообще говоря, мои заказчики предпочитают иметь возможность перемещения по экрану посредством клавиш управления курсором в любое время. Используя указатель _Curobj и приведенные ранее методы, вы действительно можете привязать структуру VALID ко всем полям для проверки того, какое поле вы только что покинули и какая клавиша при этом была нажата, а конструкция DO CASE обеспечит вам соответствующую реакцию. Но пока такая программа отработает у вас может появиться желание использовать старые подходы.

Экраны и меню в отношении один-ко-многим

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

Базовая модель для экранов с данными из связанных файлов

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

Демонстрационная программа предоставляет вам возможность выбора типа представления записей дочернего файла, соответствующих текущему состоянию рабочего экрана. (Это для программистов, не для пользователей.) Я предоставляю вам самим выбрать тип меню для вывода связанных дочерних записей. Попробуйте различные типы меню при большом числе (300-500) дочерних записей и вы увидите разницу в подходах.

Все три подходаобеспечивают достаточное быстродействие. Массивы используют память иначе чем меню pop-up, а BROWSE использует немного или совсем не использует память, выделяемую для пользовательских объектов. Меню pop-up могут иметь текст в нижней части рамки, тогда как меню на базе массивов - нет.

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

Экраны, управляемые "горячими клавишами"

Наиболее интересным нововведением FoxPro 2.0 является возможность предоставления пользователю доступа к любому экрану в любой момент времени. Использование hot-keys   заставляет программу сидеть и ждать нажатия определенной комбинации клавиш, после чего выполнить назначенное данной комбинации действие. Назначение организуется командой
ON KEY LABEL key DO program_name.

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

Когда пользователь нажимает F4 для входа в режим редактирования, перед ним немедленно появляется знакомый экран ввода данных. Это особенно удобно, потому что одновременно позволяет наблюдать остальную часть информации в родительской записи, которая слишком длинна для того, чтобы полностью быть показанной в окне BROWSE. Код приведен в листинге 1-7.

Заключение

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

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

 

Листинг 1-2


* Program.: CUST.PRG SET BLINK OFF
SET TALK OFF
SET BELL OFF
SET SCOREBOARD OFF

Plain = .T. && set to .F. to see improved screen
ColorTest = IsColor() && set to .NOT. Iscolor() to test monochrome colors

AtrBackGr = IIF ( ColorTest , [N/W*] , [W+/N,N/W] )
AtrScreen = IIF ( ColorTest , [GR+/B,N/W] , [W+/N,N/W] )
AtrData = IIF ( ColorTest , [W+/BR,W+/BR] , [WU/N,N/W] )
AtrMenu = IIF ( ColorTest , [W+/B,GR+/BG] , [W+/N,N/W] )
AtrMenu2 = IIF ( ColorTest , [N/W*,W+/R] , [W+/N,N/W] )

DEFINE WINDOW mbrowse FROM 4,15 TO 20,65 ;
PANEL SHADOW ;
CLOSE FLOAT GROW ZOOM ;
COLOR &AtrData

DEFINE WINDOW alert FROM 15,15 TO 21,65 ;
DOUBLE SHADOW ;
COLOR &AtrMenu

DEFINE WINDOW menter FROM 8,20 to 18,60 ;
DOUBLE SHADOW ;
NOCLOSE NOFLOAT NOGROW NOZOOM ;
COLOR &AtrScreen

SELECT 1
USE Customer
SCATTER MEMVAR

mdelete = ' \<Delete '
medit = .F.
mexit = .F.

IF Plain
SET COLOR TO &AtrMenu
ELSE
SET COLOR TO &AtrMenu2
ENDIF

DEFINE MENU mbartop1
DEFINE PAD mbar0 OF mbartop1 AT 0,0 PROMPT '\<File'
DEFINE PAD mbar1 OF mbartop1 AT 0,6 PROMPT '\<Go'
DEFINE PAD mbar2 OF mbartop1 AT 0,10 PROMPT '\<Record'
DEFINE PAD mbar3 OF mbartop1 AT 0,18 PROMPT '\<Utilities'
DEFINE POPUP mbar0 FROM 1,0 SHADOW
DEFINE BAR 1 OF mbar0 PROMPT ' \<Help... ' SKIP
DEFINE BAR 2 OF mbar0 PROMPT ' \<Database...' SKIP
DEFINE BAR 3 OF mbar0 PROMPT '\-'
DEFINE BAR 4 OF mbar0 PROMPT ' \<Quit '
DEFINE POPUP mbar1 FROM 1,6 SHADOW
DEFINE BAR 1 OF mbar1 PROMPT ' \<Seek ' SKIP FOR RECCOUNT() = 0
DEFINE BAR 2 OF mbar1 PROMPT ' \<Goto... ' SKIP FOR RECCOUNT() = 0
DEFINE BAR 3 OF mbar1 PROMPT ' \<Locate ' SKIP FOR RECCOUNT() = 0
DEFINE BAR 4 OF mbar1 PROMPT ' \<Continue ' SKIP
DEFINE BAR 5 OF mbar1 PROMPT '\-'
DEFINE BAR 6 OF mbar1 PROMPT ' \<Next ' SKIP FOR RECCOUNT() = 0
DEFINE BAR 7 OF mbar1 PROMPT ' \<Prior ' SKIP FOR RECCOUNT() = 0
DEFINE BAR 8 OF mbar1 PROMPT ' \<Top ' SKIP FOR RECCOUNT() = 0
DEFINE BAR 9 OF mbar1 PROMPT ' \<Bottom ' SKIP FOR RECCOUNT() = 0
DEFINE POPUP mbar2 FROM 1,10 SHADOW
DEFINE BAR 1 OF mbar2 PROMPT ' \<Edit ' SKIP FOR RECCOUNT() = 0
DEFINE BAR 2 OF mbar2 PROMPT ' \<Add '
DEFINE BAR 3 OF mbar2 PROMPT ' \<Browse ' SKIP FOR RECCOUNT() = 0
DEFINE BAR 4 OF mbar2 PROMPT '\-'
DEFINE BAR 5 OF mbar2 PROMPT mdelete
DEFINE POPUP mbar3 FROM 1,18 SHADOW
DEFINE BAR 1 OF mbar3 PROMPT ' \<Report... ' SKIP FOR RECCOUNT() = 0
DEFINE BAR 2 OF mbar3 PROMPT ' \<Label... ' SKIP FOR RECCOUNT() = 0
DEFINE BAR 3 OF mbar3 PROMPT ' \<Pack '
DEFINE BAR 4 OF mbar3 PROMPT ' Re\<index ' SKIP
DEFINE BAR 5 OF mbar3 PROMPT ' Pl\<ain '
DEFINE BAR 6 OF mbar3 PROMPT ' \<Fancy '

ON PAD mbar0 OF mbartop1 ACTIVATE POPUP mbar0
ON PAD mbar1 OF mbartop1 ACTIVATE POPUP mbar1
ON PAD mbar2 OF mbartop1 ACTIVATE POPUP mbar2
ON PAD mbar3 OF mbartop1 ACTIVATE POPUP mbar3
ON SELECTION POPUP mbar0 DO cu2_mnct WITH 0,BAR()
ON SELECTION POPUP mbar1 DO cu2_mnct WITH 1,BAR()
ON SELECTION POPUP mbar2 DO cu2_mnct WITH 2,BAR()
ON SELECTION POPUP mbar3 DO cu2_mnct WITH 3,BAR()
CLEAR

optpad = 'mbar0'

DO DispForm

DO WHILE .NOT. mexit

DO DispRec

IF Plain
SET COLOR TO &AtrScreen
ELSE
SET COLOR TO &AtrBackGr
ENDIF

IF DELETED()
@ 0,0 ;
SAY ' File Go Record Utilities ** Deleted ** '
mdelete = ' \<Recall '
ELSE
@ 0,0 ;
SAY ' File Go Record Utilities '
mdelete = ' \<Delete '
ENDIF
DEFINE BAR 5 OF mbar2 PROMPT mdelete

IF Plain
SET COLOR TO &AtrMenu
ELSE
SET COLOR TO &AtrMenu2
ENDIF

ACTIVATE MENU mbartop1 PAD &optpad

ENDDO

SET BELL ON
SET TALK ON

RETURN



PROCEDURE cu2_mnct
PARAMETER row,col

optpad = PAD()
mpop = 'mbar'+ LTRIM(STR(row))

*---Hide the menu popup

HIDE MENU mbartop1
HIDE POPUP &mpop

DO CASE

CASE row = 0 .AND. col = 4
mexit = .T.

CASE .NOT. (row=2 .AND. col=1) .AND. (RECCOUNT() = 0)
DO alert WITH [ Unrecoverable error - exiting ]
mexit = .T.

CASE row=1
DO Movement WITH col

CASE row=2 .AND. col= 1
DO EditRec

CASE row=2 .AND. col=2
DO AddRec

CASE row=2 .AND. col=3
BROWSE WINDOW mbrowse COLOR SCHEME 8

CASE row=2 .AND. col=5
IF DELETED()
RECALL NEXT 1
ELSE
DELETE NEXT 1
ENDIF

CASE row=3 .AND. col=3
choice = 1
DO alert2 WITH choice,'Not undoable - go ahead with pack?'
IF choice = 1
PACK
ENDIF

CASE row=3 .AND. col=5
Plain = .T.
DO DispForm

CASE row=3 .AND. col=6
Plain = .F.
DO DispForm

ENDCASE

DEACTIVATE MENU

RETURN



PROCEDURE DispForm

IF Plain
DO Setup1
ELSE
DO Setup2
ENDIF

SET COLOR TO &AtrScreen

@ 12, 0,15,18 BOX '+-+¦+-+¦ '
@ 2,25,18,68 BOX '+-+¦+-+¦ '
@ 10,26,12,67 BOX '+-+¦+-+¦ '

@ 13, 2 SAY [Joe's Furniture]
@ 14, 2 SAY 'Customer File'

@ 3,32 SAY 'Name '
@ 4,29 SAY 'Address '
@ 6,32 SAY 'City '
@ 7,29 SAY 'Country '
@ 7,49 SAY 'Phone '
@ 9,29 SAY 'Contact '
@ 11,27 SAY PADC('Payment Information:',40)
@ 13,30 SAY 'Last sale: '
@ 13,51 SAY 'Amount: '
@ 15,27 SAY 'Last payment: '
@ 15,51 SAY 'Amount: '
@ 17,42 SAY 'Balance '

RETURN


PROCEDURE Setup1

SET COLOR TO &AtrScreen
CLEAR

RETURN



PROCEDURE Setup2

SET COLOR TO &AtrBackGr
CLEAR
SET COLOR TO N/N
@ 13, 19, 16, 20 BOX REPL(CHR(32),9)
@ 16, 1, 16, 20 BOX REPL(CHR(32),9)
@ 3, 69, 19, 70 BOX REPL(CHR(32),9)
@ 19, 26, 19, 70 BOX REPL(CHR(32),9)

RETURN



PROCEDURE DispRec

IF Plain
SET COLOR TO &AtrScreen
ELSE
SET COLOR TO &AtrMenu2
ENDIF

@ 0, 0 SAY "Record: "+SUBSTR(STR(RECNO()+1000000,7),2)
IF DELETED()
@ 0,50 SAY "*DELETED*"
ELSE
@ 0,50 SAY " "
ENDIF

SET COLOR TO &AtrData
@ 3,37 SAY A->Name
@ 4,37 SAY A->Address1
@ 5,37 SAY A->Address2
@ 6,37 SAY A->City
@ 6,52 SAY A->State
@ 6,57 SAY A->Zip
@ 7,37 SAY A->Country
@ 7,55 SAY A->Phone
@ 9,37 SAY A->Contact
@ 13,41 SAY A->Last_sale
@ 13,59 SAY A->Lastsalamt PICTURE '99999.99'
@ 15,41 SAY A->Last_pmt
@ 15,59 SAY A->Lastpmtamt PICTURE '99999.99'
@ 17,50 SAY A->Balance PICTURE '99999.99'

RETURN


PROCEDURE EditRec
SCATTER MEMVAR
DO Gets
READ
IF .NOT. MOD(READKEY(),256) = 12
GATHER MEMVAR
ENDIF
RETURN


PROCEDURE Gets

SET COLOR TO &AtrData
@ 3,37 GET M->Name
@ 4,37 GET M->Address1
@ 5,37 GET M->Address2
@ 6,37 GET M->City
@ 6,52 GET M->State
@ 6,57 GET M->Zip
@ 7,37 GET M->Country
@ 7,55 GET M->Phone
@ 9,37 GET M->Contact
@ 13,41 GET M->Last_sale
@ 13,59 GET M->Lastsalamt PICTURE "99999.99"
@ 15,41 GET M->Last_pmt
@ 15,59 GET M->Lastpmtamt PICTURE "99999.99"
@ 17,50 GET M->Balance PICTURE "99999.99"

RETURN

PROCEDURE BlankRec
SCATTER MEMVAR BLANK
RETURN


PROCEDURE ReplRec
GATHER MEMVAR
RETURN


PROCEDURE AddRec
DO BlankRec
@ 24,0
@ 24,0 SAY "Press {Ctrl-W} to Exit"
SET COLOR TO &AtrData
DO Gets
READ
choice = " "
SET COLOR TO &AtrScreen
@ 24,0
@ 24,0 SAY "SELECT: {A}ccept {I}gnore";
GET choice PICTURE "!" VALID( choice $ "AI" )
READ
@ 24,0 CLEAR
IF choice = "A"
APPEND BLANK
DO ReplRec
ENDIF
RETURN



PROCEDURE alert
PARAMETER mess
ACTIVATE WINDOW alert
CLEAR
@ 2,0 SAY PADC(mess,WCOLS())
WAIT ''
DEACTIVATE WINDOW alert
RETURN



PROCEDURE alert2

PARAMETER answer,mess1
ACTIVATE WINDOW alert
CLEAR
@ 2,0 SAY PADC(mess1,WCOLS())
DO WHILE .T.
@ 3,3 PROMPT '< \<Yes >'
@ 3,34 PROMPT CHR(174)+' \<No '+CHR(175)
MENU TO answer
IF answer <> 0
EXIT
ENDIF
ENDDO
DEACTIVATE WINDOW alert

RETURN



PROCEDURE Movement
PARAMETER col
PRIVATE oldrecnum

* --- Determine Go choice
DO CASE

* --- Goto was chosen
CASE col=2

ACTIVATE WINDOW menter
DO dogoto
DEACTIVATE WINDOW menter

* --- Locate was chosen
CASE col=3
expr = []
GETEXPR 'Create Logical Expression:' ;
TO expr TYPE 'L;Must be a Logical Expression' DEFAULT expr
IF '' <> TRIM(expr)
SaveRec = RECNO()
LOCATE FOR &expr
IF FOUND()
DEFINE BAR 4 OF mbar1 PROMPT ' \<Continue '
ELSE
DO Alert WITH [ No match ]
GO SaveRec
ENDIF
ENDIF

* --- Continue was chosen
CASE col=4
DO docont

* --- Next was chosen
CASE col=6
IF .NOT. EOF()
SKIP
ENDIF
IF EOF()
DO alert WITH [ End of data ]
SKIP -1
ENDIF

* --- Prior was chosen
CASE col=7
IF .NOT. BOF()
SKIP -1
ELSE
DO alert WITH [ Beginning of data ]
ENDIF

* --- Top of file was chosen
CASE col=8
GO TOP

* --- Bottom of file was chosen
CASE col=9
GO BOTTOM
ENDCASE
RETURN



PROCEDURE docont
PRIVATE oldrecnum
oldrecnum = RECNO()
CONTINUE
IF EOF()
DO alert WITH [End of locate scope]
DEFINE BAR 4 OF mbar1 PROMPT ' \<Continue ' SKIP
GOTO oldrecnum
continueon = .F.
ELSE
continueon = .T.
ENDIF
RETURN


PROCEDURE DoGoTo

where = 0
recnum = 0
okcancel = 0
getnum = .F.

* --- Define and activate Goto options

@ 1,2 SAY 'Goto: '
@ 0,10 TO 9,26
@ 1,13 PROMPT ' \<Top '
@ 3,13 PROMPT ' \<Bottom '
@ 5,13 PROMPT ' \<Record '
@ 7,13 PROMPT ' \<Skip '
MENU TO where
DO CASE

*--- Goto first record
CASE where = 1
GO TOP

*--- Goto last record
CASE where = 2
GO BOTTOM

*--- Goto specified record number
CASE where = 3
recnum = 0
@ 5,29 GET recnum PICTURE '@Z 99999999'
READ
DEACTIVATE WINDOW menter
IF recnum < 1
RETURN
ENDIF
IF recnum > RECCOUNT()
DO alert WITH [ Only ] + STR(RECCOUNT(),4) + [ records in file ]
ELSE
GO recnum
ENDIF

*--- Skip specified number of records
CASE where = 4
recnum = 0
@ 7,29 GET recnum PICTURE '@Z 99999999'
READ
DEACTIVATE WINDOW menter
mrecno = RECNO()
SKIP recnum
IF EOF() .OR. BOF()
DO alert WITH msg_range
GOTO mrecno
ENDIF
ENDCASE
RETURN

PROCEDURE Alert
PARAMETERS msg
?? CHR(7)
WAIT msg WINDOW TIMEOUT 1
RETURN