Мой сайт
Меню сайта
Мини-чат
Наш опрос
Оцените мой сайт
Всего ответов: 4
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0
Форма входа
Главная » 2013 » Март » 13 » Материалы отфильтрованы по дате: Февраля 2011
23:24
 

Материалы отфильтрованы по дате: Февраля 2011

DBase и особые случаи BDE

Dbase является причиной большего количества 'special case' в BDE, чем таблицы SQL и Paradox из-за поддержки "Выражений в Индексах" (Expressions in indexes) и т.д., и т.п..

Создание/пересоздание индекса

DbiRegenIndexes( Table1.Handle ); { Регенерация всех индексов }

create index (зависит от существования выражения)

if (( Pos('(',cTagExp) + Pos('+',cTagExp) ) > 0 ) then Table1.AddIndex( cTagName, cTagExp, [ixExpression]) // <- ixExpression - _литерал_ else Table1.AddIndex( cTagName, cTagExp, []);

Мастер/Деталь связан с выражением дочернего индекса вызов BDE процедуры DbiLinkDetailToExp() вместо обычной DbiLinkDetail()

Упаковка таблиц.

with Table1 do StrPCopy( TName, TableName ); Result := DBIPackTable( DbHandle, Handle, TName, szDBASE, TRUE );

Установка видимости удаленных записей, on/off (т.е. dBase SET DELETED ON/OFF)

DbiSetProp( hDBIObj(Table1.Handle), curSOFTDELETEON, LongInt(bValue));

Установка символа частичного/точного соответствия, on/off (т.е. dBase SET EXACT ON/OFF)

DbiSetProp( hDBIObj(Table1.Handle), curINEXACTON, LongInt(bValue));

Опубликовано в DBASE и DBF

DBASE - Индексы выражений

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

В конце данного совета включены два небольших раздела, описывающих механику создания индексов выражений dBASE, один относится к утилите Database Desktop, другой - к приложениям Delphi.

Индексные выражения на основе множества полей

Функции dBASE доступны для применения в Delphi или Database Desktop для ускоренного использования в выражениях индекса, и затем только в связи с индексами dBASE. То есть, вы не сможете использовать функции dBASE или синтаксис для создания выражения индекса для таблицы Paradox или Local InterBase Server (LIBS). Функции dBASE не могут использоваться при программировании в Delphi. Они доступны только для выражений индексов dBASE. Синтаксис и функции dBASE, которые могут быть использованы для выражений индексов, "расположены" в библиотечном файле Borland Database Engine (BDE) IDDBAS01.DLL.

При создании индекса dBASE, который должен базироваться на двух или более полях таблицы, для которой он создается, два или более поля конкатенируются (связываются вместе) в величине, которая в некоторой степени похожа на Delphi тип String, с использованием синтаксиса Delphi: оператор "+". Например, выражению необходимо создать индекс, который должен базироваться в первую очередь на основе поля LastName, а затем на основе поля FirstName:

LastName + FirstName

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

У индексов с несколькими полями для других типов таблиц (например, Paradox и InterBase), используемые поля должны быть разделены точкой с запятой (;), как показано ниже:

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

LastName + FirstName

При создании индексных выражений, которые конкатенируют два и более поля, все включенные поля должны иметь одинаковый тип. К тому же, если они должны конкатенироваться, вместо складывания, то все поля должны иметь тип String. Например, для двух целочисленных полей, Value1 и Value2, выражение индекса...

...не вызовет ошибку. Но в этом случае произойдет конкатенация двух значений полей и они суммируются. Таким образом, если Value1 для данной записи содержало 4, а Value2 - 5, результирующий индексный узел будет целой величиной 9, а не строковой конкатенацией "45".

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

STR( [, [, ]]) Преобразовывает dBASE-тип Float или Numeric в Character (String) DTOS() Преобразовывает значение Date к Character, формат YYYYMMDD MLINE(, )

Извлекает отдельную строку из Memo-поля как значение Character

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

Индексные выражения на основе модификации значений полей

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

Создание индексов на основе модификации значений полей требует, по крайней мере, практическое знание функций dBASE и синтаксиса, поскольку данная технология использует dBASE, а не функции и синтаксис Delphi. Функция dBASE SUBSTR() извлекает подстроку из поля типа String. Delphi-эквивалент данной dBASE-функции - Copy. Но только dBASE функция SUBSTR() может применяться при создании индексного выражения dBASE.

Использование функций dBASE в индексных выражениях dBASE заключается в простом включении в индексное выражение функции, использование в функциях dBASE-синтаксиса и имени (имен) поля (полей), использующихся в функциях. Например, индексное выражение на основе трех последних символов значения поля типа String с именем Code, имеющим длину 20 символов выглядит так:

Важно соблюдение следующего правила: конструкции индексных выражений dBASE, модифицирующих значения полей, должны возвращать величину с "последовательной длиной" для каждой записи таблицы, т.е. результат не должен содержать граничных пробелов. Например, функция dBASE TRIM() удаляет граничные пробелы (ASCII код 32) из значения поля типа String. Если это было использовано вместе с конкатенацией двух полей, имеющих тип String, где поле не имеет постоянной длины для разных записей, длина результирующего значения будет различная для всех записей. В свете этого рассмотрим следующий пример: построим индексное выражение на основе конкатенации полей LastName и FirstName field, где функция TRIM() применена к полю LastName:

TRIM(LastName) + FirstName

Данное выражение не возвратит значения "последовательной длины" для всех записей. Если поля LastName и FirstName содержали значения...

LastName FirstName

Smith Jonas

Wesson Nancy

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

SmithJonas

WessonNancy

Как вы можете наблюдать, длина значения первого поля равна 10 символов, тогда как второго - 11 символов. Узлы индекса для данного индексного выражения должны базироваться на значении поля первой ненумерованной записи. Следовательно, результат выражения индекса для каждого узла должен быть равен 10 символов. В нашем примере результат вычисления для второй записи округляется до "WessonNanc". Все это приводит к тому, что поиск, основанный на поиске полных значений в полях, окончится неудачей.

Решение это дилеммы кроется в не использовании функции TRIM(), а в использовании полной длины значений поля LastName, включая граничные пробелы. В индексах, которые используют функции IIF() для установления порядка одного поля или другого, основанных на сравнении логических выражений в IIF(), если два поля имеют различную длину, более короткое поле должно быть заполнено пробелами до длины большей области. Для примера, создавая индекс с использованием функции IIF(), и индексируя поля Company или Name, базирующийся на поле Category, и где поле Company длиной 40 символов, а поле Name длиной 25 символов, поле Name необходимо дополнять 15-ю пробелами; например, с помощью dBASE-функции SPACE(). Выражение индекса в этом случае будет таким:

IIF(Category = "B", Company, Name + SPACE(15))

Поиск и выражения индексов dBASE

Выражения индексов dBASE являются исключениями из правил в том, как они обрабатываются Delphi и BDE, по сравнению с обработкой индексов таблиц другого типа, также основанных на множестве полей.

Это вынуждает вынести dBASE-индексы в отдельный класс. Обработка таких индексов в Delphi и BDE отличается от обработки индексов для других типов таблиц. Одно из самых существенных различий заключается в том, что не все поисковые инструменты, основанные на индексах и использующие синтаксис Delphi, могут использовать выражения индексов dBASE. FindKey, FindNearest и GotoKey методы компонента TTable не годятся для работы с выражениями индексов. При попытке использования FindKey вы получите сообщение об ошибке: "Field index out of range." (Индекс поля за границами диапазона). При попытке использования метода GotoKey может произойти та же ошибка, или табличный курсор может остаться на месте (визуально искомая величина не найдена). С выражениями индексов может использоваться только метод GotoNearest. Но даже GotoNearest может не работать с некоторыми индексными выражениями. Только с помощью эксперимента можно установить - работает ли метод GotoNearest с данным индексным выражением.

Фильтрация индексных выражений dBASE

Как и основанный на индексах поиск, индексные выражения dBASE при использовании фильтров Delphi также имеют некоторые исключения.

С активным индексным выражением метод SetRange компонента TTable приводит к следующей ошибке: "Field index out of range." (Индекс поля за границами диапазона). Тем не менее, с тем же активным индексным выражением методы SetRangeStart и SetRangeEnd успешно фильтруют набор данных.

Например, выражение индекса с конкатенацией поля LastName и активного FirstName, в приведенном ниже коде, использующем метод FindKey (предполагающий фильтрацию тех записей, где первый символ поля LastName содержит "S"), "вылетит" с ошибкой:

begin Table1.SetRange(['S'], ['Szzz']) end;

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

begin with Table1 do begin SetRangeStart; FieldByName('LastName').AsString := 'S'; SetRangeEnd; FieldByName('LastName').AsString := 'Szzz'; ApplyRange; end; end;

И, так же, как и в случае основанного на индексах поиска, успех применения фильтра целиком и полностью зависит от самого индексного выражения. Использование методов SetRangeStart и SetRangeEnd в приведенном примере работало бы с индексом, построенным на основе простой конкатенации двух полей, имеющих тип String. Но если вместо этого выражение было основано на одном или нескольких полях с использованием функции IIF(), тот же самый процесс фильтрации потерпел бы неудачу (хотя и без ошибки).

Несколько полезных советов при создании индексных выражений dBASE

Вот некоторые "удобные" индексные выражения dBASE. Некоторые интуитивно-понятные в достижении своей цели, другие немного "заумные".

Сортировка поля типа Character символов по-возрастающей, поля Date - по-убывающей

С полем типа Character и именем Name, и полем типа Date и именем OrdDate:

Name + STR(OrdDate - {12/31/3099}, 10, 0)

Сортировка поля типа Character по-возрастающей и поля типа Numeric (или Float) по-убывающей

C полем типа Character и именем Company, и полем типа Numeric и именем Amount (поле Amount имеет длину 9 цифр с двумя цифрами после десятичной запятой):

Company + STR(Amount - 999999.99, 9, 2)

Сортировка логического поля

Для того, чтобы записи со значением True располагались впереди записей со значением False в логическом поле с именем Paid, выполните следующее:

IIF(Paid, "A", "Z")

Два поля с типом Numeric (или Float)

Предположим, у нас имеется два поля типа Numeric с пятью и двумя десятичными разрядами, первое поле с именем Price, второе - Quantity:

STR(Price, 5, 2) + STR(Quantity, 5, 2)

Сортировка одного из двух полей в зависимости от выполнения логического условия

Сортировка имен месяцев в поле, имеющим тип Character

Предположим, поле содержит имена месяцев на английском языке ("Jan," "Feb" и т.д.), и его необходимо расположить в соответствующем порядке (имя поля M):

IIF(M="Jan", 1, IIF(M="Feb", 2, IIF(M="Mar", 3, IIF(M="Apr", 4, IIF(M="May", 5, IIF(M="Jun", 6, IIF(M="Jul", 7, IIF(M="Aug", 8, IIF(M="Sep", 9, IIF(M="Oct", 10, IIF(M="Nov", 11, 12)))))))))))

(Вышеприведенный код - единственная строка кода, разбирая на несколько из-за ограничений ширины страницы.)

Сортировка по первой строке Memo-поля

Для Memo-поля с именем Notes:

Сортировка по средним трем символам в девятисимвольном поле типа long

Для девятисимвольного поля типа long с именем StockNo:

SUBSTR(StockNo, 4, 3)

Создание индексных выражений dBASE в Database Desktop

В утилите Database Desktop, индексы могут создаваться как для новой таблицы (во время ее создания), так и для существующей, путем ее реструктуризации. В обоих случаях используется диалог "Define Index", использующийся для создания одного или более индексов таблицы.

Для вывода диалога создания индекса ("Create Index") во время создания новой таблицы, в диалоге создания dBASE таблицы ("Create dBASE Table") (показ структуры), выберите в списке "Table Properties" (свойства таблицы) пункт "Indexes" (индексы) и нажмите на кнопку "Define".

Чтобы вывести диалог создания индекса ("Create Index") при создании индекса для существующей таблицы, выберите Utilities|Restructure, выберите файл с таблицей в диалоге выбора файла ("Select File"), и в диалоге реструктуризации таблицы dBASE ("Restructure dBASE Table") (показ структуры таблицы) выберите в списке "Table Properties" (свойства таблицы) пункт "Indexes" (индексы) и нажмите на кнопку "Define".

Только в диалоге создания индекса ("Create Index"), выражения индекса могут создаваться щелчком на кнопке "Expression Index" (индекс выражения) и вводом выражения в поле редактирования "Expression Index". Для ассистирования данного процесса, вы можете дважды щелкнуть на имени поля с списке полей, после чего имя поля будет помещено в область редактирования "Index Expression" в текущей точке ввода (позиция курсора).

Как только нужное выражение составлено, нажмите кнопку OK. Введите имя нового индексного тэга в поле редактирования "Index Tag Name" (имя индексного тэга") в диалоге "Save Index As" (сохранить индекс как...) и нажмите "OK". (Помните, имена тэгов индексов dBASE не могут превышать десяти символов и должны соблюдать соглашения об именах dBASE.)

Создание индексных выражений dBASE в приложениях Delphi

dBASE-индексы могут создаваться программным путем в Delphi-приложениях как для новой таблицы (метод CreateTable компонента TTable), так и для существующей.

Для создания индекса как части новой таблицы, необходимо вызваться метод Add свойства IndexDefs компонента TTable. В нашем случае необходимо включить в набор флажков индекса флажок ixExpression. Данный флажок уникален для индексов таблиц dBASE, и может использоваться только с индексными выражениями dBASE. Для примера:

with Table1 do begin Active := False; DatabaseName := 'Delphi_Demos'; TableName := 'CustInfo'; TableType := ttdBASE; with FieldDefs do begin Clear; Add('LastName', ftString, 30, False); Add('FirstName', ftString, 20, False); end; with IndexDefs do begin Clear; Add('FullName', 'LastName + FirstName', [ixExpression]); end; CreateTable; end;

Добавление индекса к существующей таблицы осуществляется вызовом метода AddIndex компонента TTable. Кроме того, флажки индекса должны включать в себя значение TIndexOptions ixExpression.

Table1.AddIndex('FullName', 'LastName + FirstName', [ixExpression]);

Изучение функций и синтаксиса dBASE

Для создания индексных выражений dBASE могут использоваться только функции и синтакс, относящиеся к обработке данных. Тем не менее, полный список и описание данных функций выходит за рамки данного совета. Для получения дополнительной информации о dBASE-функциях обработки данных, обратитесь к руководству "dBASE Language Reference" или книгам и справочникам по dBASE третьих фирм.

Опубликовано в DBASE и DBF

Календарь в дбгриде

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

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

Пришлось усложнить жизнь пользователям (или облегчить) и делать так

Если нажат шифт то не под каким предлогом фокус в грид не возвращать .

Если не нажата клавиша шифт то отдаем фокус гриду.

Вот реализация.

Создадим сначала календарь ....

понадобятся глобальные переменные

Date_edit_base:TDateTimePicker; data_chebged:boolean = true; Date_edit_base_f_name:string; Date_edit_base:=TDateTimePicker.Create(DBGrid1); Date_edit_base.Parent:=DBGrid1; Date_edit_base.Left:=0; Date_edit_base.Top:=0; Date_edit_base.Visible:=false; Date_edit_base.ShowCheckbox:=false; Date_edit_base.Checked:=true; Date_edit_base.OnChange:=set_Date; // изменяем значение в гриде Date_edit_base.OnKeyDown:=datta_KeyDown; // обработка нажатий клавиш

После смены даты в календаре меняем соответственно значение поля

procedure TForm1.set_Date(Sender: TObject); begin if data_chebged then if not(( DataSet.State = dsEdit) or (DataSet.State = dsInsert)) then begin DataSet.Edit; DataSet.FieldByName(Date_edit_base_f_name).AsDateTime:=Date_edit_base.DateTime; end; data_chebged:=false; end; procedure TForm1.datta_KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); var locc:boolean; // локально обрабатывать нажатую клавишу или отдать гриду фокус begin locc:=false; if (VK_RETURN=Key) then locc:=true; if (key in [48..57]) then locc:=true; if (key in [96..105]) then locc:=true; if key=109 then key:=189; if (key = 189) then locc:=true; if ((ssShift in Shift) or (SsAlt in Shift) or (SsCtrl in Shift)) then begin // самостоятельно обработать нажатую славишу locc:=true; end; if not(locc) then begin //Фокус надо передать гриду TDateTimePicker(Sender).Visible:=false; DBGrid1.SetFocus; PostMessage(DBGrid1.Handle,WM_KEYDOWN,Key,0); Key:=0; end; if locc then if (key = 13) then push_down(Date_edit_base); end;

Процедура push_down (заставляет выпать календарик ) реализована вот так

procedure push_down(contr:TDateTimePicker ); var msg: tagEVENTMSG; begin msg.message:= WM_LBUTTONDOWN; msg.paramL:=contr.Height div 2; msg.paramH:= contr.Width - 5; msg.hwnd:=contr.Handle; contr.DefaultHandler(msg); msg.message:=WM_LBUTTONUP; contr.DefaultHandler(msg); delay(1); if contr.DroppedDown then begin contr.SetFocus; msg.message:= WM_LBUTTONDOWN; msg.paramL:= 5; msg.paramH:= 5; msg.hwnd:=contr.Handle; contr.DefaultHandler(msg); msg.message:=WM_LBUTTONUP; contr.DefaultHandler(msg); contr.SetFocus; end; end;

Также потребуется собственно нарисовать (0тбразить = переместить ) календарик в грде

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

procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); begin if (gdFocused in State) then if(( AnsiUpperCase(Column.FieldName) = AnsiUpperCase('Din') ) // тут я перечислил все свои поля типа даты or ( AnsiUpperCase(Column.FieldName) = AnsiUpperCase('UPD_DATE') )) then if not(Column.Field.DataSet.FieldByName(Column.FieldName).AsDateTime=0) then begin Date_edit_base_f_name:=Column.FieldName; // задали в глоб перем имя столбца Date_edit_base.Left := Rect.Left ; // левая точка календаря в координ грида Date_edit_base.Top := Rect.Top ; // верхняя точка Date_edit_base.Width := Rect.Right - Rect.Left + 2; // длина :) Date_edit_base.Visible := True; data_chebged:=true; // флажок что мол данные сменидися Date_edit_base.DateTime:= Column.Field.DataSet.FieldByName(Column.FieldName).AsDateTime; // записали в календарь нужное время Date_edit_base.SetFocus; // отдали фокус календ // push_down(Date_edit_base); // по желанию можно заставить его сразу и выпасть end else begin DBGrid1.Canvas.TextOut(Rect.Left+1,Rect.Top+1,'Нет данных!'); // Дата не проставлена end; end;

также по мере отладки выяснилось что надо такой финт дописать

(поймете по чему если будете делать)

procedure TForm1.DBGrid1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin IF Date_edit_base.DroppedDown THEN PostMessage(DBGrid1.Handle,WM_KEYDOWN,VK_ESCAPE,0); end; procedure TForm1.DBGrid1ColExit(Sender: TObject); begin Date_edit_base.Visible := False; end;

Опубликовано в Всё остальное

Многопоточный доступ к базам данных

По этой теме очень мало информации, особенно в части, касающейся доступа к SQL-серверам (например IB). Мне пришлось несколько дней активно заниматься всем этим – не нашел достойной замены для VirtualTree и решил заполнять дерево с помощью потока. Отмечу, что мои ранние попытки использовать потоки для обращения к Interbase не увенчались успехом, да и дискуссии по теме на форуме epsylon.public.interbase не особо вдохновляли. Жизнь заставила пересмотреть подходы к проблеме и вот что получилось.

Как известно, запрос в потоке должен выполняться в отдельном контексте, т.е. поток должен иметь как минимум IBDataset, IBTransaction и IBSQL. Можно использовать IBDataBase.InternalTransaction, но лучше таки создать в потоке отдельный IBTransaction.

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

unit basedbtr; interface uses Windows,Classes, Messages,IBDatabase,IBSQL; const {Идентификаторы сообщений, используемых для передачи информации форме, создавшей поток} WM_THREAD_TEXT=WM_USER+1000; WM_THREAD_INTEGER=WM_THREAD_TEXT+1; type TBaseDB_Thread = class(TThread) private {Локальные поля, хранящие информацию об окне владельца потока} fCurForm:HWND; {SQL текст} fNewSqlText:string; {Флаг нового запроса} fSetNewQuery:boolean; {Далее три компоненты, обеспечивающие наличие собственного контекста потока} Base:TIBdatabase; trWrite:TIbTransaction; ThreadSql:TIBSQL; {Процедуры для передачи информации владельцу потока} procedure Post_Text(MsgData:string); procedure Post_Integer(MsgData:integer); protected {Метод Execute пока ничего, кроме как перехода в состояние Suspend, не умеет} procedure Execute; override; public property CurForm:HWND read fCurForm write fCurForm; property NewSqlText:string read fNewSqlText write fNewSqlText; property SetNewQuery:boolean read fSetNewQuery write fSetNewQuery; constructor Create(OwnerHWND:HWND; //Дескриптор окна-родителя потока aBaseName:String; //DatabaseName aBaseParams:TStrings; //Информация для IBDatabase.Params TransParams:TStrings); //Информация для IBTransactin.Params destructor Destroy; override; end; {Породим от базового потока новый поток с требуемой функциональностью, которая реализуется в методе Execute} TGetRecordCount_Thread = class(TBaseDB_Thread) protected procedure Execute; override; end; {Глобальная переменная, хранящая текст для передачи окну-владельцу} var MsgText:string; implementation constructor TBaseDB_Thread.Create; begin fCurForm:=OwnerHWND; NewSqlText:=''; FreeOnTerminate :=False; base:=TIBdatabase.Create(nil); Base.DatabaseName:=aBaseName; Base.LoginPrompt:=false; Base.Params.Assign(aBaseParams); trWrite:=TIBTransaction.Create(nil); trWrite.DefaultDatabase:=Base; trWrite.Params.Assign(TransParams); ThreadSql:=TIBSQL.Create(nil); ThreadSql.SQL.Text:=''; ThreadSql.Transaction:=trWrite; Base.Connected:=true; inherited Create(true); {true - переведем поток в состояние Suspended} end; destructor TBaseDB_Thread.Destroy; begin {Остановим поток} if not Suspended then Suspend; fNewSqlText:=''; fSetNewQuery:=false; Terminate; Resume; WaitFor; ThreadSql.Free; trWrite.Free; Base.Free; inherited Destroy; end; {Ниже процедуры для передачи информации владельцу} procedure TBaseDB_Thread.Post_Text(MsgData:string); begin MsgText:=msgData; PostMessage(CurForm,WM_THREAD_TEXT,0,Integer(PChar(MsgText))); end; procedure TBaseDB_Thread.Post_Integer(MsgData:integer); begin PostMessage(CurForm,WM_THREAD_INTEGER,0,MsgData); end; {"Пустой" метод базового класса} procedure TBaseDB_Thread.Execute; begin while not Terminated do suspend; end; {Здесь Execute наследника} procedure TGetRecordCount_Thread.Execute; begin while not Terminated do begin {Если установлен флаг нового запроса} if fSetNewQuery then begin {Сбросим флаг} fSetNewQuery:=false; {Сообщим владельцу о начале работы потока } Post_text('Resume'); try if not trWrite.InTransaction then trWrite.StartTransaction; ThreadSql.Sql.Text:=NewSqlText; ThreadSql.ExecQuery; {Сообщим владельцу результат} Post_Integer(ThreadSql.Fields[0].AsInteger); trWrite.Commit; ThreadSql.Close; except raise; Post_Text('Error'); end; end; {Если в процессе выполнения не установлен флаг нового запроса, то остановимся в этой точке} if not Terminated and not SetNewQuery then begin {Вначале сообщим владельцу} Post_Text('Suspend'); suspend; end; end; end; end.

В прилагаемом к статье проекте на Delphi (10.4K) реализованы дополнительные функции для обеспечения нормальной работы потока. Рассмотрим их.

procedure TForm1.IBQuery1AfterOpen(DataSet: TDataSet); begin if Trs.Suspended then SetTrsData else Timer1.Enabled:=true; end;

Событие должно вызвать метод потока Resume для выполнения запроса о количестве записей. Но мы не знаем, стоит ли это делать, может прежний запрос касается миллионов записей и выражение WHERE столь сложен, чтоб его выполнить за секунды. Проверим состояние потока – если он Suspended, то можно сходу инициализировать поток новыми параметрами и запустить его, иначе – воспользуемся таймером Timer1.

procedure TForm1.Timer1Timer(Sender: TObject); begin if Trs.Suspended then begin Timer1.Enabled:=false; SetTrsData; end; end;

Здесь процедура рестарта потока

procedure TForm1.SetTrsData; begin Trs.NewSqlText:=Memo2.Lines.Text; Trs.SetNewQuery:=true; Trs.Resume; end;

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

procedure TForm1.IBQuery1BeforeOpen(DataSet: TDataSet); begin Timer1.Enabled:=false; end;

Вот и все. Данный пример показывает, как создавать поток для использования его в качестве дополнительного инструмента как для мелких «поручений» типа рассмотренного выше, так и для исполнения тяжелых процедур, связанных с изменениями (INSERT or UPDATE), способных ввести основной поток приложения в ступор.

Предположим, что необходимо заполнять TREE данными из запроса.

TMySql_Thread = class(TBaseDB_Thread) Private {Это флаг актуальности данных} FActual:boolean; {Набор параметров для IBSQL} FParam1:integer; FParam2:integer; protected procedure Execute; override; public property Actual:boolean read fActual write fActual; property Param1:integer read fParam1 write fParam1; property Param2:integer read fParam2 write fParam2; end; procedure TMySql_Thread.Execute; begin while not Terminated do begin {Если установлен флаг нового запроса} if fSetNewQuery then begin {Сбросим флаг} fSetNewQuery:=false; fActual:=true; {Сообщим владельцу о начале работы потока } Post_text('Resume'); try if not trWrite.InTransaction then trWrite.StartTransaction; ThreadSql.Sql.Text:=NewSqlText; ThreadSql.Params[0].asInteger:=fParam1; ThreadSql.Params[1].asInteger:=fParam2; ThreadSql.ExecQuery; {Здесь цикл вставки данных в Tree, причем анализируются Terminated и флаг актуальности потока} While not Terminated and fActual and not ThreadSql.Eof do Begin {Вызываем процедуру вставки} Synchronize(AddToList); ThreadSql.Next; End; trWrite.Commit; ThreadSql.Close; except raise; Post_Text('Error'); end; end; {Если в процессе выполнения не установлен флаг нового запроса, то остановимся в этой точке} if not Terminated and not SetNewQuery then begin {Вначале сообщим владельцу} Post_Text('Suspend'); suspend; end; end; end;

Опыт использования ADO для доступа к базам данных форматов MS Access, xBase и Paradox

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

Подвигло меня на написание этой статьи то обстоятельство, что когда я приступал к этой работе (я имею в виду использование ADO), я размещал свои вопросы во многих конференциях, а ответов на них не получено до сих пор и, более того, эти же вопросы стали задаваться по новой, а ответов на них как не было, так и нет. На некоторые из них я отвечал, а потом подумал, что не все будут просматривать конференцию целиком, да и когда все сведено в одном месте оно и лучше. Кроме того, толковой литературы по использованию ADO практически нет никакой. Например, мне не удалось найти в солидных по объему книгах г-на Архангельского необходимую мне информацию. Или еще пример - Microsoft Press 'Справочник по OLE DB'. Здесь другой уклон - информации много, слишком много, а примеров никаких (но это вообще проблема справок от Microsoft - написано много, а примеров использования почти нет).

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

Причины перехода от BDE к ADO

Итак, чтобы было понятно что к чему, сначала поясню, зачем же понадобился переход к ADO. Я работаю программистом в компании, которая занимается написанием оболочки для создания геоинформационных систем (ГИС). То есть имеется некая красивая карта и необходимо получение каких-то атрибутивных данных по объектам на этой карте размещенным. При этом атрибутивные таблицы не имеют заранее установленной структуры - только некие предустановленные поля, которых пользователь не видит, но которые используются для связи объектов на карте и записей в базе данных.

Итак, для хранения атрибутивной информации был выбран формат MS Access, который имеет то обстоятельство, что все таблицы хранятся в одном файле (в отличие от Paradox и Dbase) и не требует при этом запущенного сервера, как, к примеру, Interbase. Необходима также связь с файлами форматов dbf и db для загрузки/выгрузки данных в/из БД. Для написания программы мы используем Delphi 4, а для подключения к файлам БД использовалась BDE. И все это было отлично, но вот появились два важных обстоятельства:

· Вышел MS Access 2000. BDE отказывается работать с этим форматом. Как мне удалось найти ответ после долгих поисков на сайте Inprise - Inprise не знает как производить коннект к этому формату. Цитата: 'Для доступа к данным MS Access мы используем DAO 2.5, который не может работать с форматом MS Access 2000. Если Вам необходим доступ к БД формата MS Access 2000, используйте, пожалуйста, компоненты ADO Delphi 5. По нашей (возможно неверной) информации причина здесь в отсутствии официальной документации от Microsoft.

· Была найдена интересная особенность поведения BLOB потоков под управлением Windows NT 4. Допустим, нам необходим доступ к BLOB полям таблиц в БД формата MS Access 97. Как произвести подключение через BDE к MS Access 97 я говорить не буду, т.к. многие знают, а кто не знает, тот легко найдет эту информацию. Итак, подключение произведено. Отлично. Вот фрагмент программы:

var AStream: TBLOBStream; Data: Integer; Begin // Открываем таблицу (обычный TTable) ATable.Open; // Создаем поток. AStream := TBLOBStream(ATable.CreateBLOBStream(ATable.FieldByName('Поле'))); // Что-либо читаем из него. AStream.Read(Data, SizeOf(Data)); // Освобождаем поток и закрываем таблицу. AStream.Free; ATable.Close; end;

Казалось бы - абсолютно тривиальный код. НО! Строка, где производится чтение из потока, вызывает исключительную ситуацию - 'External error - EEFFACE'. И в исходных текстах VCL от Delphi 5 мы находим потрясающее объяснение - это, оказывается, 'C++ Exception'. Интересно, а при чем тут C++? Единственный ответ, какой я знаю, - Delphi написана на C++.

Плюс ко всему, если вы запускаете эту программу из-под Delphi - ошибка не возникает, а если запускаете ее прямо в Windows - ошибка будет непременно. Я дошел в своих поисках до вызовов прямых вызовов BDE API - вот там-то ошибка и возникает, так что я думаю тут очередная ошибка BDE, хотя я использовал на тот момент самую последнюю версию с сайта Inprise - BDE 5.11.

Так что, господа, если Вы используете нечто подобное в своих программах, то знайте, что под Windows NT 4.0/Windows 2000 Ваши программы работать не будут. Самое интересное, что компоненты из библиотеки VCL, которые используют подобный метод для получения данных (к примеру, TDBRichEdit) тоже не работают!

Итак, этих двух причин оказалось достаточно для нашей фирмы, чтобы начать переход от BDE к ADO.

ADO и файлы формата MS Access

  • —Учитель, почему ты обманул меня? Ты сказал, что Вейдер предал и убил моего отца, а теперь оказалось, что он и есть мой отец!
  • —Твой отец: Его соблазнила темная сторона силы. Он больше не был Анекином Скайукером и стал Дартом Вейдером. Поэтому хороший человек, который был твоим отцом, был уничтожен. Так что, то, что я тебе сказал, было правдой: с определенной точки зрения:
  • —С определенной точки зрения?
  • —Люк: ты вот увидишь сам: что очень многие истины зависят от нашей точки зрения.
  • (Звездные войны. Эпизод 6.)

К чему я привел эту цитату - в результате всей этой работы я пришел к выводу, что у нас, программистов, и у Microsoft разный взгляд на фразу 'Обеспечивается доступ к данным'. Мы (ну или, по крайней мере, я) в этой фразе видим следующее содержание 'обеспечивается доступ к данным для их просмотра и РЕДАКТИРОВАНИЯ (т.е. редактирование, удаление и добавление новых данных)'. Что имеет в виду Microsoft можно только догадываться, но явно, что без особых проблем достигается только просмотр данных. Кроме того, практически все примеры в литературе ограничиваются получением данных именно для просмотра, после чего следует несколько бодрых фраз и все заканчивается. Как говорится выше - разные точки зрения:

Итак, прежде всего, работа была ограничена условием разработки в Delphi 4. Причин этому много, но к этой статье это отношения не имеет. Просто - программа, разработанная в Delphi 4 должна работать через ADO. Поэтому приступили к поиску компонент, обеспечивающих такую работу. Нашли их довольно много, как платных, так и бесплатных. Все, что будет написано, одинаково и для всех вариантов и даже для Delphi5. Исключение составляет только работа с закладками в Delphi 5.

ADO была взята на тот момент самая последняя версия с сайта Microsoft - это ADO 2.6.

Итак, возьмем файл mdb формата MS Access 97. Его можно сделать с помощью хотя бы самого Access. И создадим там небольшую таблицу, к примеру, такую:

  • Object_ID Integer - идентификатор объекта на карте
  • Object_Description Text (50) - описание объекта на карте

Введем туда какие-либо данные (абсолютно все равно какие). Только надо учесть, что в силу специфики работы у нас могут быть описания, которым пока объекты не соответствуют. Такая связка будет выполнена позже пользователем. Ну, попробуем вывести содержимое таблицы в DBGrid. Ага, получилось. Например, как на картинке:

Вроде как все нормально и доступ к данным мы получили.

А теперь давайте, вообразим себя пользователями и попробуем что-нибудь исправить или добавить. Например, добавим несколько пустых записей и попробуем внести туда данные. Добавляем. Нормально. Теперь внесем данные и нажмем POST. И что мы видим?

Ага. Интересно, а при чем тут ключ, если у нас на таблицу ключ не наложен? Пробуем добавить новую запись, удалить запись без Object_ID. Результат одинаков - все то же сообщение об ошибке. И что же делать? Запускаем MS Access, пробуем там, и видим, что там все отлично. Стало быть, что-то не так мы делаем с ADO. И тут мы вспоминаем, что когда мы создавали таблицу в MS Access, он предлагал создать ключевые поля для этой таблицы. А после долгих поисков в ADO SDK я нашел этому такое объяснение: ADO предполагает, что таблица будет в первой нормальной форме. Если кто не помнит главное требование первой формы - отсутствие повторяющихся записей.

В данном случае мы не можем создать ключ на то, что есть. Что же делать? И тут приходит на ум простое решение: добавим еще одно поле, чтобы каждая запись была однозначно определена (т.е. некий внутренний идентификатор). Чтобы не думать о содержимом этого нового поля, делаем совсем просто - пусть это будет автоинкрементное поле, и создадим на него первичный ключ. Отлично! Делаем - все работает. Пока мы не добавляем больше одной записи. Если мы их добавим подряд несколько, мы увидим очень интересную ситуацию как на картинке.

Что здесь интересного? А то, что содержимое Internal_ID для всех этих записей равно нулю, хотя это автоинкрементное поле! И Table.Refresh здесь не помогает! Только закрытие и последующее открытие таблицы приводит к тому, что мы видим то, что и ожидалось.

А пока мы не имеем правильных идентификаторов, наличие такого поля не дает ничего. Выше приведенные ошибки будут продолжать сыпаться как из рога изобилия. Но вот только закрывать - открывать таблицу каждый раз после добавления новой записи для того, чтобы автоинкрементное поле принимало правильные значения - это сильно. Так не пойдет. Вот так ADO, подумал я, а давай-ка попробуем MS Access 2000. И тут оказалось, что там все нормально работает: добавляем запись, делаем сохранение (Post) автоинкрементное поле тут же принимает правильное значение.

В результате я могу сделать только один вывод - Microsoft активно, всеми доступными средствами, пытается заставить пользователей переходить к своим новым продуктам.

А вот почему в Access все нормально работает - это загадка. Я думаю, что сам-то он пользуется какими-то своими методами, либо в процессе работы у него есть некий идентификатор записи типа только что придуманного нами.

Ну а чтобы пользователь не видел этого внутреннего идентификатора (он ведь нужен только нам) делаем это поле невидимым. Надеюсь, что все знают, что это делается через TField.Visible := FALSE.

Кто-нибудь может возразить: а зачем нам такой идентификатор, мы можем записи идентифицировать по каким-нибудь своим полям. Ради Бога! Но тут есть еще одна проблема и эта проблема называется закладки.

Проблемы закладок нет в Delphi 5, потому что там вокруг Bookmark сделан класс ими управляющий, а я имею в виду работу с закладками через ADO. Смотрим опять же в ADO SDK и видим там такое описание:

'Recordset.Bookmark: Устанавливает или возвращает закладку, которая однозначно определяет текущую запись в Recordset. При создании или открытии объекта Recordset каждая из его записей получает уникальную закладку. Для того чтобы запомнить положение текущей записи, следует присвоить текущее значение свойства Bookmark переменной. Для быстрого возвращения к сохраненному в переменной указателю текущей записи в любое время после перехода на другую запись следует указать в значении свойства Bookmark объекта Recordset значение этой переменной'.

Казалось бы, какие проблемы? А вот какие: возвращаемое значение всегда одно и тоже для любой записи. И когда мы устанавливаем этот, с позволения сказать, Bookmark, ничего не происходит. И только наш внутренний идентификатор поможет в такой ситуации, кроме того, его значение всегда имеет смысл, даже после закрытия и повторного открытия таблицы, что, в общем-то, удобно.

После того как все заработало, я решил проверить скорость работы ADO. У нас может быть ситуации, когда в таблицу добавляется сразу большое количество записей, к примеру, 50-60 тысяч записей за раз. Так вот, когда использовалась BDE, такая операция занимала максимум 10 минут. Угадайте, чему стало равно это время при использовании ADO? Минимум 25 минут на той же самой машине. Если после этого мне будут говорить, что ADO быстрее BDE чуть ли не в 2 раза - позвольте мне с Вами не согласиться.

Итак, для нормальной работы мы должны иметь таблицы в первой нормальной форме, для этого делаем автоинкрементное поле с уникальным индексом. Кроме того, если мы можем добавлять больше одной записи за один раз и потом сразу возможно будем их редактировать, нам надо использовать файлы MS Access 2000.

ADO и файлы xBASE и Paradox

Итак, мы смогли наладить работу через ADO к файлам формата MS Access. Но ведь мы можем и должны использовать файлы xBase и Paradox в качестве обменных файлов.

Попробуем это сделать. Все примеры какие я видел в книгах работают одинаково - через 'Microsoft OLE DB provider for ODBC'. А все редакторы, которые делают строку подключения, всегда показывают только mdb файлы в диалоге, в котором задается путь к файлу БД. Что-то тут нечисто, подумал я - а как же тот же самый Access это делает? Ведь явно не через ODBC, стало быть, есть какая-то хитрость.

После примерно недельных поисков в Интернете решение было найдено. Да, действительно можно использовать 'Microsoft Jet 4.0 OLE DB Provider'. Чтобы не рассказывать долго, представим, что у нас на диске D в корне лежит файл Test.dbf формата dBase 5.0.

Строка коннекта для этого случая будет выглядеть так:

'Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\;Extended Properties=dBase 5.0; mode=Read|Write|Share Deny None;Persist Security Info=True';

И это все.

Самое интересное во всей это строке - секция 'Extended Properties'.

Чтобы знать, что конкретно для разных форматов надо писать в Extended properties, загляните в реестр Windows на следующую ветку:

HKEY_LOCAL_MACHINE\Software\Microsoft\Jet\4.0\ISAM Formats

Там перечислены все поддерживаемые в данном случае форматы.

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

А вот формат Paradox - это оказалась песня на меньшая, чем mdb. И вот почему - здесь все требования о первой форме таблицы в действии, но ведь мы не можем создавать таблицу, потом говорить пользователю 'Слышь, мужик, а теперь метнулся, запустил Paradox и создал первичный ключ на эту таблицу. А потом нажмешь на ОК и мы продолжим'. Это несерьезно. Стало быть, этот ключ надо создавать нам самим.

Хорошо, запускаем справку по MS Jet SQL и ищем раздел создания индексов или первичных ключей. Находим следующее:

CREATE INDEX имя_индекса ON название_таблицы (название_поля) WITH PRIMARY. ALTER TABLE название_таблицы ADD CONSTRAINT имя_ограничения PRIMARY KEY (название_поля)

Все далее сказанное абсолютно одинаково для обоих вариантов.

Предположим, что наша таблица называется ExpTbl.db и поле, на которое мы хотим наложить первичный ключ, называется IntrernalID. Хорошо, подключаемся к таблице и задаем такую строку SQL для исполнения:

CREATE INDEX My_Index ON ExpTable (InternalID) WITH PRIMARY

Запустим на выполнение. Ого, а что это мы видим? Вот те на - очередное сообщение об ошибке. При этом сообщение как всегда очень содержательное применительно к нашему случаю.

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

Вывод один - опять очередное требование ADO, которое сразу не поймешь. Ладно, запускаем он-лайн MS MSDN и делаем запрос на PARADOX. Видим что-то около 50 документов. И где-то в 35-36 документе я нашел ответ маленькими буковками внизу экрана! Сейчас я вам скажу в чем проблема - держитесь крепче: имя первичного ключа должно совпадать с названием таблицы, а имена индексов с именами полей. Неслабо.

Исправляем SQL:

CREATE INDEX ExpTable ON ExpTable (InternalID) WITH PRIMARY

Запускаем, смотрим - все отлично.

Чтобы никто больше мучился с этим делом, я хотел бы привести самые значащие ограничения для драйвера PARADOX, которые я нашел в MSDN:

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

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

Итак, для работы через ADO с файлами xBase или Paradox, нам необходимо указывать нужный драйвер в секции Extended Properties и в секции Data Source только путь до файла. Для xBase на этом все трудности закончены, а вот для Paradox необходимо задание первичного ключа как для формата MS Access, при этом есть определенные ограничения при задании названий ключей, так же как и возможных индексов.

То, о чем речь пойдет далее уже не относится к организации работы с таблицами xBase и Paradox через ADO, а скорее упоминание об одном полезном опыте.

Для добавления данных в эти таблицы, мы можем вставлять их по одной (Table.Append (Insert); Table.Post), а можем воспользоваться вариантом SELECT : INTO, INSERT : INTO. Поговорим теперь именно о втором варианте работы.

Смотрим файл справки MS Jet SQL.

SELECT поле_1 [, поле_2 [, ...]] INTO новаяТаблица [IN внешняяБазаДанных] FROM источник

Ладно, пробуем. Пусть мы имеем в качестве источника данных mdb файл и хотим сохранить данные из таблицы SourceTable в таблицу формата Paradox 7.0 TestTable.db, расположенную в корне диска D:.

Казалось бы:

SELECT * INTO [TestTable.DB] IN 'D:\' FROM Source Table

Нет, очередная ошибка. Вот, что мы видим.

Ага, хорошо, давайте попробуем указать таблицу в пути:

SELECT * INTO [TestTable] IN 'D:\ TestTable.DB' FROM Source Table

Получим очередное сообщение об ошибке.

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

SELECT * INTO [Paradox 7.x;DATABASE=D:\].[TestTable#DB] FROM SourceTable

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

Самое потрясающее это название раздела MSDN, где я нашел этот ответ - 'Как, используя ADO, открыть таблицу Paradox, защищенную паролем'. Как ЭТО имеет отношение к этому синтаксису SQL, я так и не понял, честно говоря.

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

При написании статьи использовались следующие материалы:

· Материалы Королевства Delphi.

· Справочные файлы Delphi 4 и Delphi 5.

· Исходные коды VCL Delphi 4 и Delphi 5.

· MS ADO SDK и примеры MS ADO SDK.

· MS MSDN.

· А.Я. Архангельский 'Язык SQL в Delphi 5'.

Опубликовано в ADO

Моя собственная база данных

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

Постановка задачи: Допустим, мне нужно в приложении Delphi сохранять некоторую информацию на диск. Мне не охота работать с текстовыми файлами, так как просмотр и обновление информации в них довольно муторное занятие. Преобладать будут операции записи и чтения, в то время как операции изменения и апдейта будут присутствовать в меньшей степени. Вся информация будет хранится в переопределённом типе данных Pascal Record. Итак, какой подход мне лучше всего использовать?

BDE плюс Paradox или Access, ... спасибо, не надо...Не хотелось бы испытывать мороку с BDE. Использовать текстовые файлы ASCII ? Не пойдёт. Нужна хоть какая-то минимальная защита, а текстовые файлы "полностью видимы". Оказывается, ответ на данный вопрос кроется в Delphi, а именно в непечатных файлах (или файлы некоторых типов/бинарные файлы).

var container = document.getElementById('nativeroll_video_cont'); if (container) { var parent = container.parentElement; if (parent) { const wrapper = document.createElement('div'); wrapper.classList.add('js-teasers-wrapper'); parent.insertBefore(wrapper, container.nextSibling); } }

Просмотров: 410 | Добавил: elizablen | Рейтинг: 0.0/0
Всего комментариев: 0
Поиск
Календарь
«  Март 2013  »
Пн Вт Ср Чт Пт Сб Вс
    123
45678910
11121314151617
18192021222324
25262728293031
Архив записей
Друзья сайта
  • Официальный блог
  • Сообщество uCoz
  • FAQ по системе
  • Инструкции для uCoz
  • Copyright MyCorp © 2025
    Бесплатный конструктор сайтовuCoz