- 締切済み
Excel VBA 非表示の最後のセルを取得
ExcelのVBAで最後のセル・行・列を取得しようとしているのですが、 実際の最後(値や数式が入っている)のセルが非表示の列や行だった場合、 期待する最後を取得できないでいます。 取得しようとして試した方法は、以下の2つですが、 いずれも非表示している直前の行列までしか返さないようです。 ・Cells.SpecialCells(xlCellTypeLastCell) ・UsedRange 他に最後のセルを取得する方法があれば教えてください。 現在の目的は、ブック全体からすべての数式を取得することで、 全シートの最初のセルから最後のセルまでを見て、 =で始まるセルを数式があるとして処理しています。 最後のセルが非表示の行・列だった場合、 その行・列までを見る事ができなかったので気づきました。 今回の目的以外でも、全シートの最後のセルまで、と言う処理は行ないたいので、 これはこれで回答がいただきたいのと、 他に、ブック全体からすべての数式を取得する方法があれば、 それはそれで回答していただけると助かります。 ただ、非表示列を一旦表示してから取得し、その後非表示に戻す、と言う方法は、 そのブックを閉じる時に変更があったとみなされ、 保存の確認メッセージが表示されるので、したくありません。 確認メッセージを表示させない、と言う方法も、 万一、その有効/無効が完結されない場合があって、 閉じる際に保存したかったのにメッセージが表示されないために保存できなかった、 と言う事になりたくないので、したくありません。 無理なら無理と言う回答でも構いませんが、 何かヒントをいただけるとありがたいです。
- みんなの回答 (6)
- 専門家の回答
みんなの回答
- real beatin(@realbeatin)
- ベストアンサー率82% (174/211)
No.2-3 です。 もはやお呼びじゃないような気もしてますし、蛇足にはなりますが、 「現在の目的」を具体的に知ることが出来ましたから、 「今回の目的以外」のことには触れずに、これまでのお話を総合・整理して 「現在の目的」に特化した形で、もう一度だけ回答してみます。 いや、質問者さんには相当高いレベルで書き上げる力があると思いますから、 こちらの意図としては、マクロの提示というよりは、 仕様の提案、設計の参考、といった感じですので、 軽い気持ちで読んでやってください。 同一アプリケーション内の他ブックに対応可能なこと 同種の数式を(複数セル範囲毎に)纏めること(No.2より変更なし) 定義された名前の使用を前提に先頭(左上)セルの数式をそのまま返すこと 非表示セル範囲に数式があれば、フラグを立ててListBoxに渡すこと、 listbox.List プロパティに二次元配列を渡すこと 対象ブックが未保存状態になることなく処理すること 保護されたシートに対しても漏れなくトレースできるようにすること UserFormはモードレスで表示すること ListBoxでリスト選択すると、セル範囲にジャンプすること ListBoxを右クリックすると、非表示セルのリストに変更すること などです。目視での確認作業をサポートする意味合いでの設計です。 とりあえず、UserForm1にListBox1を用意するぐらいでも簡単に試せます。 なんか厚量になってきましたからこの件特にレスなくて構いませんので、 ご自在に。 ' ' ーー Sub ReW9157303_r() Dim oDict As Object ' R1C1形式の数式文字列で同種の数式をを纏める目的の 連想配列(Scripting.Dictionary) Dim wks As Worksheet ' Worksheet ループ用 Dim rngFornulas As Range ' 数式が設定されたセル範囲 Dim c As Range ' Cell ループ用 Dim sShN As String, sF As String ' シート名 , R1C1形式の数式文字列を取得 Dim blnSaved As Boolean, bH As Boolean ' ブック保存済 , 各セルの非表示 ' ' 未保存なら False 保存済なら True blnSaved = ActiveWorkbook.Saved Set oDict = CreateObject("Scripting.Dictionary") For Each wks In ActiveWorkbook.Worksheets ' ' シート保護 適用時は VBAからの処理に限って保護を解除 If wks.ProtectContents Then ' ◆要指定(確認)Password ↓ If wks.ProtectionMode = False Then wks.Protect Password:="", UserInterfaceOnly:=True End If ' ' シート名 sShN = wks.Name ' ' 数式が設定されたセル範囲 On Error Resume Next Set rngFornulas = wks.Cells.SpecialCells(xlCellTypeFormulas) On Error GoTo 0 ' ' 〓 各シートのすべての数式 If rngFornulas Is Nothing Then ' Debug.Print sShN, "数式が設定されたセル無し" Else For Each c In rngFornulas ' ' 同種の数式を纏める目的で R1C1形式の数式文字列を取得 sF = c.FormulaR1C1 If sF <> "" Then ' ' セルの非表示を判定 bH = c.EntireRow.Hidden Or c.EntireColumn.Hidden sF = sShN & vbCr & sF ' ' 数式文字列をR1C1形式で評価して既に取得済の数式と同種であるかチェック If oDict.Exists(sF) Then ' ' 同種の数式が設定されたセル範囲をUnionで纏める oDict(sF) = VBA.Array("", oDict(sF)(1), Union(wks.Range(oDict(sF)(2)), c).Address(0, 0), oDict(sF)(3)) Else ' ' dictionary のItemに配列を格納(非表示フラグ, シート名, セル参照, 数式) oDict(sF) = VBA.Array("", sShN, c.Address(0, 0), c.Formula) End If If bH Then ' 非表示セルだった場合 sF = "Hidden" & sF If oDict.Exists(sF) Then oDict(sF) = VBA.Array("Hid〓", oDict(sF)(1), Union(wks.Range(oDict(sF)(2)), c).Address(0, 0), oDict(sF)(3)) Else oDict(sF) = VBA.Array("Hid〓", sShN, c.Address(0, 0), c.Formula) End If End If End If Next End If Set rngFornulas = Nothing Next ' ' 処理前に保存済だったなら、処理後も保存済に戻す If blnSaved Then ActiveWorkbook.Saved = blnSaved ' ' 〓 ListBox リスト設定 Const COL_WD = "27;50;150;250" ' ListBox 各列幅 With UserForm1.ListBox1 ' ◆要指定(確認)UserForm名,ListBox名 .IntegralHeight = True ' お約束 .ColumnCount = 4 .ColumnWidths = COL_WD ' ' 二段階配列を二次元配列にコンバートしてListBoxのListを設定 .List = Application.Transpose(Application.Transpose(oDict.Items)) .Tag = ActiveWorkbook.Name End With Set oDict = Nothing: Set c = Nothing UserForm1.Show vbModeless ' ◆要指定(確認)UserForm名, End Sub ' ' ーー ' ' ーー UserFormモジュール ' ' UserForm1 ' ' > ListBox1Box1 ' ' .ColumnCount = 4 [非表示フラグ(文字の有無)] [シート名] [セル参照] [数式] ' ' ListBox上でリスト選択すると、セル範囲にジャンプする Private Sub ListBox1_AfterUpdate() ' ◆要指定(確認)ListBox名 With ListBox1 ' ◆要指定(確認)ListBox名 Application.Goto Workbooks(.Tag).Sheets(.List(.ListIndex, 1)).Range(.List(.ListIndex, 2)) If .List(.ListIndex, 0) <> "" Then MsgBox "Hidden!!" & vbLf & .List(.ListIndex, 2) End With End Sub ' ' ListBoxを右クリックすると、非表示セルの数式リストに Private Sub ListBox1_MouseUp(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single) ' ◆要指定(確認)ListBox名 Dim i As Long If Button <> 2 Then Exit Sub ' 右クリック以外は無視 With Me.ListBox1 ' ◆要指定(確認)ListBox名 For i = .ListCount - 1 To 0 Step -1 If .List(i, 0) = "" Then .RemoveItem (i) Next i End With End Sub ' ' ーー
- kagakusuki
- ベストアンサー率51% (2610/5101)
>結合したセルに数式がある場合に問題がありました。 >SpecialCells(xlCellTypeFormulas >では、結合されたセルが個別に取得されてくるようですが、 >それぞれのセルには数式(Formula)は入ってないのです。 これは失礼致しました。 まさか結合セルの左上端以外のセルまで取得されてしまうとは思ってもみませんでした。 では、少々改良致しまして以下の様なVBAでは如何でしょうか? Sub QNo9157303_Excel_VBA_非表示の最後のセルを取得_改() Dim ws As Worksheet, c As Range, i As Long, n As Long, buf() As String On Error Resume Next ReDim buf(2, 0) i = -1 For Each ws In Sheets temp = ws.Name n = 0 With ws.Cells.SpecialCells(xlCellTypeFormulas, 23) n = .Cells.Count If n > 0 Then ReDim Preserve buf(2, i + n) For Each c In .Offset(0) If c.Formula <> "" Then i = i + 1 buf(0, i) = .Parent.Name buf(1, i) = c.Address(False, False) buf(2, i) = c.Formula End If Next c End If End With Next ws On Error GoTo 0 If i >= 0 Then ReDim Preserve buf(2, i) End Sub
お礼
追加でご回答いただきありがとうございます。 私のために時間をさいてくだすって申し訳ありません。 回答用のプログラミングを楽しまれていらっしゃると幸いです。 新しい呪文の登場ですね。 .Offset(0) ヘルプを読んでもピンとこないのが私のいけないところですが、 ヘルプよりも人さまのプログラムの方がよっぽど役立つと、 それを色々つつき回して動作の違いを試して理解するのも、 また楽しからずや。 これは自分の座標からの相対位置を返すような感じですかね。 追加の質問ではないので、よっぽどの間違いでなければ、 ご回答は不要ございますので、お気づかいなく。 そうすると、 .Offset(0,0) または、 .Offset() の方がもっともらしいかも知れませんね。 最初は結合セルの左上を取得するためかと思いましたが、 取得した数式のオブジェクトを With 化したために、 自分自身を参照するための記述かと解釈しました。 With の便利と不便(VB6で使用した頃に注意が必要でした)が、 悩ましいところです。 . だけで自分自身が参照できたらいいかも知れませんが、 コードの可読性が下がるかもと、自提案自却下したりして。 一旦、別のオブジェクトに保持すると言うてもあるかもと思って、 こんなんにしてみました。 Sub QNo9157303_Excel_VBA_非表示の最後のセルを取得_改_Ver2() Dim ws As Worksheet, c As Range, i As Long, n As Long, buf() As String Dim cc As Range 'オブジェクト保持用 On Error Resume Next ReDim buf(2, 0) i = -1 For Each ws In Sheets temp = ws.Name n = 0 Set cc = ws.Cells.SpecialCells(xlCellTypeFormulas, 23) '←オブジェクト保持 n = cc.Cells.Count If n > 0 Then ReDim Preserve buf(2, i + n) For Each c In cc If c.Formula <> "" Then i = i + 1 buf(0, i) = ws.Name '←.Parent.Name改め buf(1, i) = c.Address(False, False) buf(2, i) = c.Formula End If Next c End If Next ws On Error GoTo 0 If i >= 0 Then ReDim Preserve buf(2, i) Stop 'buf内容確認のため止め End Sub あまり使いたくない(わがまま)のが、 On Error Resume ただ xlCellTypeFormulas が何も返さないとエラーになるので、 どうしても必要そうですね。 配列の用意、拡張も悩ましいです。 いつも似たような事をしているのですが、 VBAにこの利便性を求めてはいけないのでしょうね。 バージョンアップされるものではないですし、 Excelからなくならないだけマイクロソフトの良心だと。 ソースコードのやりとりの場にしてすみません。 ご回答とお礼の繰り返しに慣れて、 なれなれしい言葉づかいになってしまったのもすみません。
- kagakusuki
- ベストアンサー率51% (2610/5101)
回答No.1です。 ブック内に存在する全ての「関数が入力されているセル」の「関数」、「セル番号」、及び「そのセルが存在しているシートのシート名」を、配列変数に格納するVBAを考えましたので、一例として参考にして下さい。 尚、配列変数bufの内、 buf(0,○)には「該当セルが存在しているシートのシート名」が格納され、 buf(1,○)には「該当セルのセル番号」が格納され、 buf(2,○)には「該当セルに入力されている関数」が格納される様になっております。 Sub QNo9157303_Excel_VBA_非表示の最後のセルを取得() Dim ws As Worksheet, c As Range, i As Long, n As Long, buf() As String On Error Resume Next ReDim buf(2, 0) i = 0 For Each ws In Sheets temp = ws.Name n = 0 n = ws.Cells.SpecialCells(xlCellTypeFormulas, 23).Cells.Count If n > 0 Then ReDim Preserve buf(2, UBound(buf, 2) + n + (i = 0)) For Each c In ws.Cells.SpecialCells(xlCellTypeFormulas, 23) buf(0, i) = c.Parent.Name buf(1, i) = c.Address(False, False) buf(2, i) = c.Formula i = i + 1 Next c End If Next ws On Error GoTo 0 End Sub
お礼
追加でご回答いただきありがとうございます。 フォームにリストボックスを配置して、 そこに数式の一覧を表示しようとしていたので、 配列変数 buf() の次元の方向を変えて、 リストボックスの .List に代入すると完成しました。 ---- 不要な数式や名前が定義され、 はたまたシェイプなどの画像(これも見えなくなっている)もあったりで、 目には見えず、何かしらの邪魔をしでかしかねないものがてんこ盛り。 そんなExcelブックが、 何を元にされたのかわからないまま使いまわされ、 仕事の業務内であちこちへと飛び回っています。 ともすると、 \\サーバー名\共有名\フォルダ名\ファイル名 を参照するような名前や数式がまぎれていて、 そこへ直接アクセスする権限がないとしても、 かようなものがよそ様へ出回るのもどうかと思って、 私なりに不要なものを除くツールのようなものをこしらえていました。 シェイプや名前はなんとなくできていましたし、 数式もできていたつもりでしたが、 いままで表示されていなかったのに、 とあるセルに値を入力したとたん表示され始めた数式が出てきて、 なぜだろうかと調べていたら、 Cells.SpecialCells(xlCellTypeLastCell) の罠にはまったわけです。 とあるセルと言うのが、非表示列より右側のセル。 よくよくシートの列ヘッダーを見ると、 非表示にされて狭まっている列を発見。 L、M、N、O、、U、うん? P、Q、R、S、Tはどこへ? 列を再表示してみるとずらりと数式の計算結果が表示されている。 そんなわけで今回の質問とあいなりました。 今回の質問で、SpecialCellsのオプション、 XlCellTypeが今までよりもわかり、勉強になりました。 ヘルプを読んで「数式が含まれているセル」とあっても、 それが自分が求めているものかどうかの判断もつかず見過ごし、 インターネットで検索するも「Excel数式一覧」では、 関数の一覧ばかりが出てきて、目的を達成できず。 誰もかような問題に出くわしていないのか、 調べるまでもなく解決しているのか、 私の無知をさらす結果となりましたが、 ご回答いただいたお二方には、 わざわざソースコードまでいただき、感謝しています。
補足
結合したセルに数式がある場合に問題がありました。 SpecialCells(xlCellTypeFormulas では、結合されたセルが個別に取得されてくるようですが、 それぞれのセルには数式(Formula)は入ってないのです。 結合セルのうち左上にのみ数式が入っているので、 If c.Address = c.MergeArea(1, 1).Address Then ※結合されていなくても MergeArea は同じ位置を返すようなので、 MergeCells で結合されているかどうかまでは判断してません。 あるいは、 If c.Formula <> Empty Then とすることで、数式のないセルは無視するようにしました。 数式が入っているセルを取得しようとしているのに、 その数式が入っていないと言うのも妙なExcel。 同じ数式が入っているか、 結合セルの左上以外は返さないようになっているとよかったですのに、 マイクロソフトさん、と言いたくなりました。
- real beatin(@realbeatin)
- ベストアンサー率82% (174/211)
No.2 です。書き忘れがありましたので追加します。 Cells.CurrentRegion とか、 Range("A1").CurrentRegion とか、 もし、シートの保護が適用されていなくて、 それぞれのシート上で、 先頭セルから最終セルまで、空の行や空の列を挟まない標準的なレイアウトであれば、 .CurrentRegionは、 非表示のセルを含めて、値のある一連なりのセル(単矩形)範囲返してくれます。 場合によっては役に立つかも、です。
お礼
追加でご回答いただきありがとうございます。 ひとつらなり、と言うところが重要ですね。
- real beatin(@realbeatin)
- ベストアンサー率82% (174/211)
こんにちは。お邪魔します。 全部一纏めにお応えしますが、こんな感じで如何でしょう。 結果はイミディエイトウィンドウ(VBEで Ctrl + G)に表示されます。 ' ' /// Option Explicit Sub ReW9157303() Dim oDict As Object ' R1C1形式の数式文字列で同種の数式をを纏める目的の 連想配列(Scripting.Dictionary) Dim wks As Worksheet ' Worksheet ループ用 Dim rngFornulas As Range ' 数式が設定されたセル範囲 Dim rngValues As Range ' 固定値や数式が設定されたセル範囲 Dim a As Range ' Area ループ用 Dim c As Range ' Cell ループ用 Dim v ' Dictionaryオブジェクトの Key ループ用 Dim sF As String ' 同種の数式を纏める目的で R1C1形式の数式文字列を取得 Dim nLastRow As Long, nLastCol As Long ' 固定値や数式が設定された最終セルの 行位置 列位置 Set oDict = CreateObject("Scripting.Dictionary") For Each wks In ActiveWorkbook.Worksheets Debug.Print wks.Name ' ' 〓 固定値や数式が設定された最終セル nLastRow = 0: nLastCol = 0 On Error Resume Next ' ' 数式が設定されたセル範囲 Set rngFornulas = wks.Cells.SpecialCells(xlCellTypeFormulas) ' ' 固定値または数式が設定されたセル範囲 If rngFornulas Is Nothing Then Set rngValues = wks.Cells.SpecialCells(xlCellTypeConstants) Else Set rngValues = Application.Union(rngFornulas, wks.Cells.SpecialCells(xlCellTypeConstants)) End If On Error GoTo 0 If rngValues Is Nothing Then Debug.Print , "固定値や数式が設定されたセル無し" Else ' ' 固定値または数式が設定されたセル範囲の各矩形範囲を総当たりループ For Each a In rngValues.Areas ' ' 各矩形範囲の右下セルの行・列位置を取得して最大値を出す。 With a(a.Count) If .Row > nLastRow Then nLastRow = .Row If .Column > nLastCol Then nLastCol = .Column End With Next ' ' 〓 固定値や数式が設定された最終セル↓ Debug.Print , "最終セル:"; wks.Cells(nLastRow, nLastCol).Address(0, 0) End If ' ' 〓 各シートのすべての数式 If rngFornulas Is Nothing Then If Not rngValues Is Nothing Then Debug.Print , "数式が設定されたセル無し" Else For Each c In rngFornulas ' ' 同種の数式を纏める目的で R1C1形式の数式文字列を取得 sF = c.FormulaR1C1 ' ' 数式文字列をR1C1形式で評価して既に取得済の数式と同種であるかチェック If oDict.Exists(sF) Then ' ' 同種の数式が設定されたセル範囲をUnionで纏める Set oDict(sF) = Union(oDict(sF), c) Else Set oDict(sF) = c End If Next Debug.Print , "▼数式適用範囲", "▼数式" For Each v In oDict.keys ' ' 〓 数式適用範囲(の.Address)〓 R1C1形式からA1形式に再変換した数式 Debug.Print , oDict(v).Address(0, 0), """"; Application.ConvertFormula(v, xlR1C1, xlA1); """" Next oDict.RemoveAll End If Set rngFornulas = Nothing: Set rngValues = Nothing Next Set oDict = Nothing End Sub ' ' /// > ただ、非表示列を一旦表示してから取得し、その後非表示に戻す、と言う方法は、 いや、その方が簡単に済ませられるような事をお望みならば、 workbook.Saved プロパティを操作すればいいです。 ' ' /// Dim blnSaved As Boolean blnSaved = ActiveWorkbook.Saved ' 未保存なら False 保存済なら True ' ' ◆ココ例えば、行・列を、非表示・再表示などの処理 ' ' 非表示・再表示の処理だけでブックが未保存状態になってしまうのを避ける意味で ' ' 処理前に保存済だったなら、処理後も保存済に戻す If blnSaved Then ActiveWorkbook.Saved = blnSaved ' ' /// 多分、今回お訊ねの件については、このやり方の方が 処理の仕方を複数選べるようになるので、色々と融通が利くように思いますけれど、 まぁ実際にそちらで書いてみて、扱い易い方法を選んでみて下さい。 何か不明、不足、あれば、補足してみて下さい。 以上です。
お礼
ご回答いただきありがとうございます。 教えていただきました、 Cells.SpecialCells(xlCellTypeFormulas) で現在の目的は達成できました。 非表示の行・列でも、何かしら入っていれば、 Cells.SpecialCells(xlCellTypeConstants) が使えそうな事もわかりました。 変更がなかったかのように見せかける、 ActiveWorkbook.Saved も勉強になりました。 否定的だった再表示/再非表示もいいかと思いましたが、 自分で提案しておいて面倒、効果的でない事に気づきました。 どこが非表示かを記録しておく必要がありますが、 それをどこの行・列まで探すかとなると、 結局、最後のセルが必要となるか、 でなければ、Excelの最大行・列まで繰り返す必要があり、 たぶん、その処理に時間がかかるような気がしたので、ボツ案でした。
- kagakusuki
- ベストアンサー率51% (2610/5101)
例えば関数が入力されている全てのセルの個数を求めるのでしたら Cells.SpecialCells(xlCellTypeFormulas, 23).Cells.Count というVBAの関数で出来ますから、関数が入力されている全てのセルの個数を変数に格納して取得するのであれば、 変数名 = Cells.SpecialCells(xlCellTypeFormulas, 23).Cells.Count という構文になります。 又、関数が入力されている全てのセルに対して、それらのセルを1つずつ指定して何らかの処理を行うのであれば、 Dim c As Range MsgBox Cells.SpecialCells(xlCellTypeFormulas, 23).Cells.Count For Each c In Cells.SpecialCells(xlCellTypeFormulas, 23) (処理) Next c という構文の中のFor Each~Next c内において変数cに対して行いたい処理を行う様にされると良いと思います。 ですから、例えば「関数が入力されている全てのセルのアドレスを、Msgboxを使用して1つずつ表示させる」という場合には次の様なVBAになります。 Dim c As Range MsgBox Cells.SpecialCells(xlCellTypeFormulas, 23).Cells.Count For Each c In Cells.SpecialCells(xlCellTypeFormulas, 23) MsgBox c.Address Next c 又、「関数が入力されている全てのセルの関数を、Msgboxを使用して1つずつ表示させる」という場合には次の様なVBAになります。 Dim c As Range MsgBox Cells.SpecialCells(xlCellTypeFormulas, 23).Cells.Count For Each c In Cells.SpecialCells(xlCellTypeFormulas, 23) MsgBox c.Formula Next c
お礼
ご回答いただきありがとうございます。 教えていただきました、 Cells.SpecialCells(xlCellTypeFormulas で現在の目的は達成できました。
お礼
追加でご回答いただきありがとうございます。 私のために時間をさいてくだすって申し訳ありません。 仕様の提案までしてくだすってありがとうございます。 最初にいただいたソースコードでは、 仕様までがわからなかったので、 なぜなぜこうなんだろうと思いながら、 質問から話がそれそうなのでそのままにしていました。 仕様の提案で、ずいぶんとソースコードの読み方が変わりました。 同種の数式を纏める、が少々気になっていました。 やたらめったらリストボックスに表示されるのもどうかと思いながら、 まあそんなものだろう、と言うのが私の仕様でした。 同種の判定がR1C1形式なのがさらに気になりました。 R1C1形式だと相対的な表現なので、 A1セルの数式:=A2 B1セルの数式:=B2 は纏められてしまいます。 最初は、 えー、=A2 と =B2 は違うんだから別々にしてよ と思いましたが、本来の目的は、 サーバー名が入っていたり、不要だと思われる数式の検索なので、 問題のない数式などはどうもしないのだから、 纏められていた方がいいのかなと思いました。 非表示セル範囲に数式があれば、フラグを立てて、が便利でした。 リストボックスをクリックしたらセルへ移動するのはやっていたのですが、 非表示セルに移動しても、シート側ではわかりにくかったので、 リストボックス側で知れると注意してシートを見れそうです。 最終的にどのようなプログラムになったかはおいておいて、 おふたりのおかげさまで目的が達成でき、 何よりも勉強になりました。 知ってるつもりでも知らない事や、 想像もしない、できないような事がたくさんあるもので、 人から教わると、それが少しずつ見えてきて、 色々と再認識させていただく事ができました。