Замена меток в Word2000 текстом (необходима альтернатива)

Программирование на Visual Basic for Applications
Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

Замена меток в Word2000 текстом (необходима альтернатива)

Сообщение Rojohn » 29.03.2006 (Ср) 0:37

Есть такая задачка: В шаблоне Word имеются метки вида <Text1>. Причем, они могут находиться как просто в тексте как внутри шейпов (надписей) Word и иногда в одной надписи могут быть несколько меток. Как из VB60 заменить текстовыми результатами запросов соответствующие метки на бланке, с учетом того, что метки могут повторяться и скорость вывода бланка была минимальной?
Мой вариант такой (работает очень долго):
Код: Выделить всё
Dim w1 As Word.Application, w2 As Word.Document
  Dim shp As Word.Shape
  Dim rsBlancP As Recordset
  Dim rsShp As Recordset, rsLbl As Recordset
  Dim rsTxt As Recordset, rsTbl As Recordset
  Dim rsTblReq As Recordset, rsTblReq1 As Object
  Dim strRez As String, NullMe As String
  Dim n_col As Integer, n_pp As Long

  Set rsBlancP = dbsCurrent.OpenRecordset("SELECT * FROM PRINT_BLANCS WHERE ((FILE_NAME='" & valBlanc & "') AND (ID=" & BlancID & "))")
    If rsBlancP.EOF = False Then
      Set w1 = CreateObject("Word.Application")
        w1.documents.Add Template:=(PathBlanc + valBlanc)
        Set w2 = w1.ActiveDocument
          Set rsLbl = dbsCurrent.OpenRecordset("SELECT * FROM PRINT_TEXT")
            If rsLbl.EOF = False Then
              rsLbl.MoveFirst
              Do While Not rsLbl.EOF
                If rsLbl![N_FIELDS] = 0 Then                                   w2.ActiveWindow.Selection.Find.ClearFormatting
                  w2.ActiveWindow.Selection.Find.Replacement.ClearFormatting
                  w2.ActiveWindow.Selection.Find.text = rsLbl![TXT_TITLE]
                  w2.ActiveWindow.Selection.Find.Execute Replace:=wdReplaceAll, ReplaceWith:=SelectReq(rsLbl![REQ_NUM]), Wrap:=wdFindContinue
                  For Each shp In w2.Shapes
                    shp.Select
                    w2.ActiveWindow.Selection.Find.ClearFormatting
                    w2.ActiveWindow.Selection.Find.Replacement.ClearFormatting
                    w2.ActiveWindow.Selection.Find.text = rsLbl![TXT_TITLE]
                    w2.ActiveWindow.Selection.Find.Execute Replace:=wdReplaceAll, ReplaceWith:=SelectReq(rsLbl![REQ_NUM]), Wrap:=wdFindContinue
                    With shp.TextFrame
                      If .HasText Then
                        .TextRange.Select
                        w2.ActiveWindow.Selection.Find.ClearFormatting
                        w2.ActiveWindow.Selection.Find.Replacement.ClearFormatting
                        w2.ActiveWindow.Selection.Find.text = rsLbl![TXT_TITLE]
                        w2.ActiveWindow.Selection.Find.Execute Replace:=wdReplaceAll, ReplaceWith:=SelectReq(rsLbl![REQ_NUM]), Wrap:=wdFindContinue
                      End If
                    End With
                   Next
                 Else
                  NullMe = SelectReq(rsLbl![REQ_NUM])
                  If valADO = False Then
                    Set rsTblReq = dbsCurrent.OpenRecordset(NullMe)
                      If rsTblReq.EOF = True Then
                        rsLbl.close
                        Set w2 = Nothing
                        w1.Visible = True
                        Set w1 = Nothing
                        rsBlancP.close
                        Exit Sub
                      End If 'If rsTblReq.EOF = True
                      w1.WordBasic.StartOfDocument
                      w1.WordBasic.EditFind Find:=CStr(rsLbl![TXT_TITLE])
                      Do While w1.WordBasic.EditFindFound()
                        With w1.WordBasic
                          .Insert tblHider
                          w2.ActiveWindow.Selection.TypeParagraph
                          rsTblReq.MoveFirst
                          n_pp = 1
                          Do While Not rsTblReq.EOF
                            NullMe = n_pp
                            For n_col = 0 To (rsTbl![N_FIELDS] - 1)
                              If IsNull(rsTblReq.Fields(n_col)) = False Then
                                NullMe = NullMe + ";" + CStr(rsTblReq.Fields(n_col))
                              Else
                                NullMe = NullMe + ";" + ""
                              End If
                            Next n_col
                            .Insert NullMe
                            w2.ActiveWindow.Selection.TypeParagraph
                            n_pp = n_pp + 1
                            rsTblReq.MoveNext
                          Loop 'rsTblReq
                          .ParaUp rsTblReq.RecordCount + 1, 1
                          w2.ActiveWindow.Selection.ConvertToTable Separator:=wdSeparateByCommas, NumColumns:=rsTbl![N_FIELDS] + 1, NumRows:=rsTblReq.RecordCount + 1, Format:=wdTableFormatGrid1, ApplyBorders:=True, ApplyShading:=True, ApplyFont:=True, ApplyColor:=True, ApplyHeadingRows:=True, ApplyLastRow:=False, ApplyFirstColumn:=True, ApplyLastColumn:=False, AutoFit:=False, AutoFitBehavior:=wdAutoFitFixed
                          w2.ActiveWindow.Selection.ParagraphFormat.Alignment = wdAlignParagraphCenter
                          w2.ActiveWindow.Selection.MoveDown Unit:=wdLine, Count:=1, Extend:=wdMove
и т.д.

Но он работает долго...
Есть словарь меток, хранящийся в БД Access 2000. Этот словарь прогоняется, ищутся соответствия на бланке и заменяется соответствующим текстовым результатом запроса.
Подскажите какой-нить более быстрый алгоритм. Очень актуальный вопрос для меня! :?:

GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 29.03.2006 (Ср) 5:10

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

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 29.03.2006 (Ср) 9:48

GSerg
Шаман, подскажи плз, как это сделать?
Не совсем уверен, что это ляжет в мой контекст, но мысль хорошая... может и ляжет :)

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 29.03.2006 (Ср) 14:09

GSerg
Немного прочитал про слияние базы данных с шаблоном Word... Не дошел до того как это делается, но понял, что это мне не подойдет, т.к. при слиянии устанавливается связь с метками в Word и при удалении метки удалится и поле в б.д. Если это не так, то подскажи как делать слияние. Если так, то нужен просто быстро работающий код для замещения текстовых меток в Word текстом из VB (во всех казанных выше случаях, т.е. текст, надписи и метки в надписях ну хотя бы в одном из случаев:)).
:?:

alibek
Большой Человек
Большой Человек
 
Сообщения: 14205
Зарегистрирован: 19.04.2002 (Пт) 11:40
Откуда: Russia

Сообщение alibek » 29.03.2006 (Ср) 14:25

Прочитай еще, потому что ты понял совсем неправильно.
Документ слияния -- это шаблон, который при слиянии заполняется данными из источника данных.
Lasciate ogni speranza, voi ch'entrate.

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 29.03.2006 (Ср) 16:56

Теперь всё понял вроде насчет слияния... но мне не подходит, т.к. метки всё равно ищутся по названию для каждого бланка отдельно. А мне надо, чтобы при подключении любого документа Word на его странице искались метки, названия которых присутствуют в таблице БД Access и заменялись на текст в том же словаре (текст соответствует определенной метке словаря, а вообще-то там будет ссылка на запрос). Т.е. мы заранее не знаем, какие метки из словаря будут стоять на бланке. Как :?:

alibek
Большой Человек
Большой Человек
 
Сообщения: 14205
Зарегистрирован: 19.04.2002 (Пт) 11:40
Откуда: Russia

Сообщение alibek » 29.03.2006 (Ср) 16:58

Формируй документ слияния программно. Все-равно правильнее будет, чем делать самопальное слияние.
Lasciate ogni speranza, voi ch'entrate.

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

хм...

Сообщение Rojohn » 30.03.2006 (Чт) 1:19

Формируй документ слияния программно. Все-равно правильнее будет, чем делать самопальное слияние.

Я подумаю... Только увеличит ли это скорость вывода бланка?
Потом я не делаю "самопального слияния", а просто пытаюсь произвести поиск и замену меток текстовыми результатами запросов из VB.
А самое главное: нужно это делать не зная заранее, какие метки из словаря меток попадутся на странице Word и внутри чего они там будут стоять (текст, надпись, метки внутри надписи).

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 31.03.2006 (Пт) 15:11

И всё же, господа! Вопрос остается открытым... Честно говоря я не понял, как сделать слияние метки в Word c текстом, допустим из переменной в VB, при условии, что метка в Word может стоять где угодно (текст, надпись, таблица) и заранее не известно, какие метки из словаря меток будут присутствовать на бланке...

GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 31.03.2006 (Пт) 15:18

Перед поиском значения всех меток добавить в коллекцию с ключом "<Код метки>". Потом for eacn w in thisdocument.words, if incollection(w.text) then w.text=col(w.text), next.
Как только вы переберёте все варианты решения и не найдёте нужного, тут же обнаружится решение, простое и очевидное для всех, кроме вас

Alex123
Начинающий
Начинающий
 
Сообщения: 13
Зарегистрирован: 16.11.2005 (Ср) 7:14

Сообщение Alex123 » 02.04.2006 (Вс) 7:26

Могу предложить метод заполнения бланков, которым пользуюсь сам.
В программе сначала данные из базы записываются в текстовый файл, где номер строки есть номер поля в базе. Потом открывается WORD и нужный бланк, бланк заполняется через несложный макрос (каждая строка из текстового файла заменяет соответствующий текст типа <5> через обычный поиск и замену.
В чем я вижу преимущества этого метода. Во-первых, уменьшается код самой программы, во-вторых, совместимость с любой версией WORD-а, в-третьих, одно поле из базы может быть вставлено несколько раз.

GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 02.04.2006 (Вс) 9:02

Почему я не вижу преимуществ этого метода? :scratch:

Зачем текстовый файл - прямой проход можно делать и в базе?
Почему простой макрос - как ты будешь находить текст внутри shape?
Почему совместимость лучше?
Почему одно поле нельзя вставлять несколько раз по-другому?
Как только вы переберёте все варианты решения и не найдёте нужного, тут же обнаружится решение, простое и очевидное для всех, кроме вас

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 03.04.2006 (Пн) 10:16

GSerg
Перед поиском значения всех меток добавить в коллекцию с ключом "<Код метки>". Потом for eacn w in thisdocument.words, if incollection(w.text) then w.text=col(w.text), next.

1) Как определить w? Как объектную переменную, означающую слово в Word? Как именно?
2) Будет ли это работать, если вместо коллекции наименования меток будут браться из рекордсета в цикле?
3) thisdocument - это встроенное слово VB или имя моей переменной, обозначающей документ Word?

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

А почему не работает это?

Сообщение Rojohn » 03.04.2006 (Пн) 11:52

А почему из VB не работает это?
Код: Выделить всё
w2.ActiveWindow.Selection.Find.ClearFormatting
w2.ActiveWindow.Selection.Find.Replacement.ClearFormatting
With w2.ActiveWindow.Selection.Find
   .text = "<ФИО>" 'rsLbl![txt_title]
   .Replacement.text = "Иванов  И.И." 'SelectReq(rsLbl![REQ_NUM])
   .Forward = True
   .Wrap = wdFindContinue
   .Format = False
   .MatchCase = False
   .MatchWholeWord = False
   .MatchWildcards = False
   .MatchSoundsLike = False
   .MatchAllWordForms = False
End With
w2.ActiveWindow.Selection.Find.Execute Replace:=wdReplaceAll


Хотя из Word всё и везде заменяется (макрос создан из Word)?

alibek
Большой Человек
Большой Человек
 
Сообщения: 14205
Зарегистрирован: 19.04.2002 (Пт) 11:40
Откуда: Russia

Сообщение alibek » 03.04.2006 (Пн) 12:26

Потому что Option Explicit использовать надо.
Lasciate ogni speranza, voi ch'entrate.

GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 03.04.2006 (Пн) 12:31

1. А посмотри по F2, объекты какого типа возвращает Item в коллекции Words.
2. Да на здоровье. Всего-то во сколько-то там десятков раз медленнее.
3. А посмотри по F2.
Как только вы переберёте все варианты решения и не найдёте нужного, тут же обнаружится решение, простое и очевидное для всех, кроме вас

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 03.04.2006 (Пн) 13:15

alibek
Потому что Option Explicit использовать надо


Option Explicit стоит... ничего не говорит, но и ничего не заменяет...

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 03.04.2006 (Пн) 13:31

GSerg
1. А посмотри по F2, объекты какого типа возвращает Item в коллекции Words.

Тип-то Long, а какой индекс ставить у Item? Или нужен ещё цикл по ним? Если да, то как он будет выглядеть, я чего-то не понимаю..

GSerg
3. А посмотри по F2.

Посмотрел... ничего не нашло. А должно было? :)

alibek
Большой Человек
Большой Человек
 
Сообщения: 14205
Зарегистрирован: 19.04.2002 (Пт) 11:40
Откуда: Russia

Сообщение alibek » 03.04.2006 (Пн) 13:31

В таком случае проверь, что константа wdReplaceAll определена правильно.
Lasciate ogni speranza, voi ch'entrate.

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 03.04.2006 (Пн) 13:57

alibek

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


Как проверить эту встроенную константу VBA? Разве возможны другие варианты определений? Причём в другом случае она работает, но этот случай сюда не подходит...

GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 03.04.2006 (Пн) 14:13

Rojohn
Я тебе показал, как выглядит цикл. Кодом.
И тип переменной long откуда ты взял, я понять не могу. Нажми f2 ещё раз.
Как только вы переберёте все варианты решения и не найдёте нужного, тут же обнаружится решение, простое и очевидное для всех, кроме вас

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 03.04.2006 (Пн) 14:34

GSerg
Если имеется ввиду сам Item, то он Range. И как тогда его задавать?
Может легче написать тип, а то я уже не знаю куда копать...
По идее надо написать что-то типо Set w=w2.Words.Item(Index As Long) As Range. Чему будет равен Index? Или не так?

GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 03.04.2006 (Пн) 14:43

Rojohn писал(а):Если имеется ввиду сам Item, то он Range. И как тогда его задавать?
Может легче написать тип, а то я уже не знаю куда копать...

Круто.
"Дядя Ваня, а как вас зовут?"

Тип - Range, Rojohn. Именно Range. Dim w as Range. 2+2=4. Да.


Rojohn писал(а):По идее надо написать что-то типо Set w=w2.Words.Item(Index As Long) As Range. Чему будет равен Index? Или не так?

Я думаю, пришла пора открыть учебник по основным синтаксическим конструкциям VB. Там, где про if, for и do. После основательного изучения указанного пособия нужно повторно прочитать мой пост и увидеть в нём строчку "for eacn w in thisdocument.words, if incollection(w.text) then w.text=col(w.text), next". Которая является кодом. Написанным через запятую, чтобы не делать разрывы строк. Ы?
Как только вы переберёте все варианты решения и не найдёте нужного, тут же обнаружится решение, простое и очевидное для всех, кроме вас

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 03.04.2006 (Пн) 16:24

Пишу:
Код: Выделить всё
Dim w As Range
Set w1 = CreateObject("Word.Application")
w1.documents.Add Template:=(PathBlanc + valBlanc)
Set w2 = w1.ActiveDocument
Set w = w2.Range
...
For Each w In w2.Words
If w.text = rsLbl![txt_title] Then w.text=SelectReq(rsLbl![REQ_NUM])
Next

-> ничего не вставляется... Что не так?
P.s.: Слова thisdocument не нашел нигде

GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 03.04.2006 (Пн) 16:30

Забавно.

Метка типа <text> считается тремя словами.
Тогда можно искать "<", и если найдено, смотреть на следующее и ещё следующее w.


ЗЫ. Если без коллекции, смысл метода исчезает.

ЗЗЫ. Set w = w2.Range убрать.
Как только вы переберёте все варианты решения и не найдёте нужного, тут же обнаружится решение, простое и очевидное для всех, кроме вас

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 03.04.2006 (Пн) 16:49

И всё-таки, что не так в коде:
Код: Выделить всё
w2.ActiveWindow.Selection.Find.ClearFormatting
w2.ActiveWindow.Selection.Find.Replacement.ClearFormatting
With w2.ActiveWindow.Selection.Find
   .text = "<ФИО>" 'rsLbl![txt_title]
   .Replacement.text = "Иванов  И.И." 'SelectReq(rsLbl![REQ_NUM])
   .Forward = True
   .Wrap = wdFindContinue
   .Format = False
   .MatchCase = False
   .MatchWholeWord = False
   .MatchWildcards = False
   .MatchSoundsLike = False
   .MatchAllWordForms = False
End With
w2.ActiveWindow.Selection.Find.Execute Replace:=wdReplaceAll

Это бы решило все мои проблемы...

GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 03.04.2006 (Пн) 17:09

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

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

Когда ты ищёшь в диалоге поиска ручками, поиск происходит везде. А когда из кода, то только в основном слое. Без шейпов. И без фреймов. Причём текст внутри фреймов образует wdTextFrameStory, и потому в них можно искать через thisdocument.StoryRanges(wdTextFrameStory).find, а текст внтури шейпов - нет. Их по одному всё равно перебирать.

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

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 03.04.2006 (Пн) 22:44

GSerg
Метка типа <text> считается тремя словами.


А если всё же сделать коллекцию, то будет читаться одним словом?
ЗЫ:
GSerg, неужели ты сам никогда подобное не делал? Как ты выходил из такой ситуации с Word, способом с коллекцией или ещё как?

GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 04.04.2006 (Вт) 5:14

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


ЗЫ: Представь нет, слиянием обходились.
ЗЗЫ: Было одно исключение. Одна досовская прога, используемая в бюджетной области до сих пор, имеет отчёты в формате rtf. Так вот там метки в файле были вида @$Метка$@, и когда прога делала отчёт, она тупо открывала файл rtf как текст и тупо заменяла replace'ом метки значениями. rtf такое обращение с собой позволяет.
Как только вы переберёте все варианты решения и не найдёте нужного, тут же обнаружится решение, простое и очевидное для всех, кроме вас

Rojohn
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 439
Зарегистрирован: 22.06.2005 (Ср) 11:00
Откуда: Moscow city

?

Сообщение Rojohn » 04.04.2006 (Вт) 7:30

GSerg
Коллекцию, Rojohn, ты будешь делать из своей базы с метками, а не из слов Word.

Это-то понятно, но при записи:
if incollection(w.text) then

получается, будет искаться элемент коллекции, равный очередному слову из коллекции Words, в которой, как ты сказал, элементы "<", "название метки" и ">" будут считаться отдельно. Теоретически он просто не найдет в коллекции моих меток элемент "<", так как там будет только "<название метки>".

ЗЫ: А через слияние можно сделать подобное? Или не стоит городить огород с устанавливанием подключения к бланку Word для каждой метки. Потом, мне кажется, что слияние можно делать только тогда, когда точно известны имена меток, существующие на бланке...

След.

Вернуться в VBA

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 20

    TopList